Đây là cái bẫy được nhắc nhiều nhất trong cộng đồng Go, và nó gây ra lỗi thật trong mã sản xuất. Bài này tái hiện nó và giải thích cơ chế.

Hiện tượng

var p *Meo = nil
var i Noi = p
  p == nil : true
  i == nil : false     <<< dù i chứa đúng con trỏ nil đó

Con trỏ là nil. Gán nó vào interface. Interface không nil.

Vì sao

Bài 14 đã đo: interface chiếm 16 byte, và đó là hai con trỏ — một trỏ tới thông tin kiểu, một trỏ tới dữ liệu.

  interface nil thật  ->  (kiểu = nil,   giá trị = nil)
  i sau khi gán p     ->  (kiểu = *Meo,  giá trị = nil)

i == nil chỉ đúng khi cả hai ô đều nil. Sau khi gán p, ô kiểu chứa *Meo — nên interface khác nil, dù dữ liệu bên trong là nil.

Nói cách khác: interface nhớ kiểu của thứ bạn bỏ vào, kể cả khi thứ đó rỗng.

Chỗ nó thật sự cắn: trả về error

Đây mới là dạng gặp trong mã thật, và nó tệ hơn nhiều:

type MyErr struct{}
func (m *MyErr) Error() string { return "loi" }

func traVeLoi(hong bool) error {
	var e *MyErr           // con trỏ, giá trị nil
	if hong {
		e = &MyErr{}
	}
	return e               // trả về CON TRỎ, tự động bọc thành error
}
  traVeLoi(false) -> err == nil ?  false     <<< BẪY
  errors.Is(err, nil)              false

Hàm chạy đúng đường "không có lỗi", trả về một con trỏ nil — và người gọi kiểm if err != nil thì thấy có lỗi.

Đây là lỗi im lặng nhất trong Go: mã biên dịch sạch, không panic, và đường xử lý lỗi chạy trong khi không có lỗi nào. Với API HTTP, nó thành 500 cho một request hoàn toàn bình thường.

Và nó khó tìm vì fmt.Println(err) in ra <nil> — nhìn log thì thấy "không có lỗi", nhưng err != nil vẫn đúng.

Ba cách phòng

Khai kiểu trả về là error, không phải kiểu cụ thể:

func traVeLoi(hong bool) error {
	if hong {
		return &MyErr{}
	}
	return nil            // nil THUẦN, không qua biến con trỏ
}

Đây là cách chuẩn. Trả nil trực tiếp thì cả hai ô đều nil.

Đừng dùng biến con trỏ kiểu cụ thể làm nơi chứa lỗi. Mẫu sai điển hình:

var e *MyErr           // <- nguồn gốc vấn đề
if ... { e = &MyErr{} }
return e

Nếu buộc phải, hãy kiểm trước khi trả:

if e != nil { return e }
return nil

Đừng khai hàm trả về kiểu lỗi cụ thể:

func f() *MyErr   // <- người gọi gán vào error là dính bẫy ngay

Luôn trả error. Đây là một trong số ít chỗ Go khuyến khích trả interface thay vì struct, và lý do chính là cái bẫy này.

Nó không chỉ xảy ra với error

Bất kỳ interface nào cũng dính:

func layWriter(co bool) io.Writer {
	var w *bytes.Buffer
	if co { w = &bytes.Buffer{} }
	return w              // cùng vấn đề
}

Nên quy tắc tổng quát: khi trả về interface, hãy trả nil tường minh chứ đừng trả một biến con trỏ có thể nil.

Cách kiểm khi đã lỡ

Nếu bạn nhận một interface từ mã người khác và nghi ngờ:

fmt.Printf("%T %v\n", i, i == nil)

%T in ra kiểu thật. Thấy *Meoi == nilfalse trong khi giá trị in ra <nil> — bạn vừa bắt được nó.

Trong mã sản xuất thì dùng phản chiếu, nhưng nó chậm và xấu:

reflect.ValueOf(i).IsNil()

Cần tới nó thường là dấu hiệu nên sửa chỗ tạo ra interface thay vì chỗ nhận nó.

Vì sao Go không sửa

Câu hỏi hợp lý: sao không cho i == nil trả về true khi giá trị bên trong nil?

Vì như thế sẽ mất khả năng phân biệt "không có gì" với "có một *Meo rỗng". Có mã dựa vào việc gọi method trên con trỏ nil — điều này hợp lệ trong Go nếu method không truy cập trường:

func (m *Meo) Noi() string { return "meo" }   // không đụng m
var p *Meo
p.Noi()   // chạy bình thường

Nên hành vi hiện tại là hệ quả của một quyết định thiết kế khác, không phải lỗi. Nhóm Go coi đây là chỗ tài liệu phải cảnh báo, và go vet có một số kiểm tra cho trường hợp rõ ràng nhất.

Thử ba mươi giây

func f() error { var e *MyErr; return e }
fmt.Println(f() == nil, f())

In ra false <nil>. Hai thứ mâu thuẫn nhau trên cùng một dòng.

Ba mươi giây đó là lý do quy tắc "luôn return nil tường minh" tồn tại trong mọi hướng dẫn Go.

Ngày mai: zero value và cách thiết kế API quanh nó.