0

Go Zero to Hero - Bài 24: Trả về nhiều giá trị từ một Hàm (Multiple Return Values)

Trong các ngôn ngữ như C++ hay Java, việc một hàm cần trả về cả "Kết quả tính toán" lẫn "Trạng thái lỗi" thường đòi hỏi dùng con trỏ, tham chiếu hoặc ném ngoại lệ (exception). Go chọn một con đường thanh lịch hơn rất nhiều: Hỗ trợ trả về N giá trị trực tiếp ngay trong chữ ký hàm.


1. Tại sao Go lại cần tính năng này?

Thay vì phải tạo ra các object rườm rà hay dùng cơ chế throw/catch làm gián đoạn luồng chạy, Go cho phép bạn trả về nhiều giá trị cùng lúc.

package main

import "fmt"

// Cú pháp: Bọc các kiểu trả về trong cặp ngoặc đơn ()
func processStationData(stationID int) (string, int, bool) {
    stationName := "Ga Trung Tâm"
    activeTVMs := 5 // Số máy bán vé đang hoạt động
    isOnline := true
    
    // Trả về theo đúng thứ tự đã khai báo
    return stationName, activeTVMs, isOnline
}

func main() {
    name, tvms, status := processStationData(101)
    fmt.Printf("Ga: %s | Số máy: %d | Online: %t\n", name, tvms, status)
}

2. Cặp bài trùng kinh điển: (Result, Error)

Dù bạn có thể trả về 3, 4 hay 10 giá trị, nhưng trong 99% dự án thực tế, bạn sẽ bắt gặp một chuẩn mực (idiom) thống nhất của cộng đồng Gopher: Hàm luôn trả về 2 giá trị - Giá trị kết quả và Lỗi (error).

Cách này ép lập trình viên phải đối mặt và xử lý lỗi ngay lập tức, không được phép lảng tránh hay đẩy trách nhiệm cho hàm khác.

package main

import (
    "errors"
    "fmt"
)

// Trả về kết quả giao dịch (string) và lỗi nếu có (error)
func deductFare(cardID string, amount float64) (string, error) {
    if amount <= 0 {
        // Giao dịch không hợp lệ, trả về chuỗi rỗng và đối tượng lỗi
        return "", errors.New("số tiền trừ phải lớn hơn 0")
    }
    
    // Logic gọi sang hệ thống thanh toán (giả lập)
    if cardID == "BLACKLISTED_CARD" {
        return "", errors.New("thẻ nằm trong danh sách đen")
    }
    
    // Thành công: Trả về mã giao dịch và nil (không có lỗi)
    transactionID := "TXN-998877"
    return transactionID, nil
}

func main() {
    // Luôn luôn hứng và check err ngay sau khi gọi hàm
    txnID, err := deductFare("BLACKLISTED_CARD", 15000)
    
    if err != nil {
        fmt.Println("🚨 Giao dịch thất bại:", err)
        return // Dừng luồng xử lý tại đây
    }
    
    fmt.Println("✅ Giao dịch thành công, mã:", txnID)
}

3. Blank Identifier (_): Nghệ thuật lơ đi những thứ không cần thiết

Quy tắc thép của Go: Bạn phải hứng TẤT CẢ giá trị trả về của hàm, và hứng rồi thì PHẢI sử dụng chúng.

Nhưng nếu hàm trả về 2 giá trị, mà bạn chỉ quan tâm đến 1 giá trị thì sao? Dấu gạch dưới _ tiếp tục đóng vai trò là vị cứu tinh.

// Giả sử hàm này trả về (dữ_liệu, lỗi)
func fetchCache() (string, error) {
    return "Dữ liệu cũ", nil
}

func main() {
    // Chúng ta chỉ cần dữ liệu, không thèm quan tâm có lỗi hay không (Ví dụ cache lỗi thì vẫn lấy tạm)
    data, _ := fetchCache()
    
    fmt.Println("Dữ liệu lấy được:", data)
}

⚠️ Lưu ý: Việc dùng _ để lờ đi biến error là một Bad Practice cực kỳ nguy hiểm trong môi trường Production. Chỉ nên dùng khi bạn chắc chắn 100% lỗi đó không ảnh hưởng đến hệ thống, hoặc dùng để bỏ qua các tham số phụ (không phải lỗi).


4. Named Return Values và Cạm bẫy "Shadowing" chết người

Như đã nhắc tới ở các bài trước, bạn có thể đặt tên trước cho các biến trả về. Tuy nhiên, tính năng này ẩn chứa một cái bẫy kinh hoàng mang tên Variable Shadowing (Che khuất biến). Rất nhiều kỹ sư kinh nghiệm vẫn phải gãi đầu gãi tai debug hàng giờ vì nó.

Hãy xem đoạn code có vẻ rất bình thường sau:

package main

import (
    "errors"
    "fmt"
)

// Khai báo sẵn biến trả về: count và err
func countActiveUsers() (count int, err error) {
    count = 100 // Gán giá trị bình thường
    
    // MỘT VÒNG LẶP HOẶC KHỐI IF BẤT KỲ CÓ SỬ DỤNG :=
    if true {
        // 🚨 BẪY SHADOWING XUẤT HIỆN Ở ĐÂY!
        // Dấu := tạo ra một biến 'err' MỚI TINH, cục bộ chỉ nằm trong khối IF này.
        // Nó "che khuất" biến 'err' trả về của hàm.
        err := errors.New("lỗi kết nối database rớt mạng")
        fmt.Println("Log cục bộ:", err)
    }
    
    // Lệnh return rỗng sẽ trả về biến 'err' ở CHỮ KÝ HÀM (lúc này vẫn đang là nil)
    return 
}

func main() {
    c, e := countActiveUsers()
    fmt.Printf("Số user: %d | Lỗi: %v\n", c, e)
    // KẾT QUẢ IN RA: Số user: 100 | Lỗi: <nil>
    // Ơ kìa, lỗi Database đã biến mất hoàn toàn???
}

Cách khắc phục:

Khi đã dùng Named Return Values, hãy cực kỳ cẩn thận với toán tử := bên trong hàm. Nếu muốn cập nhật giá trị cho biến trả về, hãy dùng phép gán = thông thường.

// Sửa lại đoạn trong khối IF
if true {
    // Dùng dấu = để gán cho biến err của hàm, KHÔNG dùng :=
    err = errors.New("lỗi kết nối database rớt mạng") 
}

Tổng kết

Việc làm chủ tính năng Multiple Return Values định hình nên phong cách code chuẩn (Idiomatic Go) của bạn:

  1. Cặp đôi hoàn hảo: 99% các hàm nghiệp vụ đều nên trả về (ResultType, error).
  2. Kỷ luật xử lý lỗi: Luôn dùng if err != nil ngay sau khi gọi hàm. Trông có vẻ lặp đi lặp lại (boilerplate), nhưng nó bảo vệ hệ thống khỏi những cú crash bí ẩn.
  3. Cẩn thận bẫy Shadowing: Nếu dùng Named Return Values (Naked Return), tuyệt đối cẩn thận không khởi tạo lại tên biến đó bằng := ở các khối scope bên trong (như if, for) làm mất kết quả thực tế.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.