Lỗi con trỏ nil
Mở đầu
Trong quá trình viết code, tôi cần gọi phương thức Close() để đóng nhiều đối tượng, giống như code dưới đây
type A struct {
b B
c C
d D
}
func (a A) Close() error {
if a.b != nil {
if err := a.b.Close(); err != nil {
return err
}
}
if a.c != nil {
if err := a.c.Close(); err != nil {
return err
}
}
if a.d != nil {
if err := a.d.Close(); err != nil {
return err
}
}
return nil
}Nhưng viết nhiều phán đoán if như vậy cảm thấy không tao nhã, B, C và D đều đã triển khai phương thức Close, có lẽ có thể đơn giản hơn, nên tôi đã cho chúng vào một slice, rồi vòng lặp phán đoán
func (a A) Close() error {
closers := []io.Closer{
a.b,
a.c,
a.d,
}
for _, closer := range closers {
if closer != nil {
if err := closer.Close(); err != nil {
return err
}
}
}
return nil
}Như vậy có vẻ tao nhã hơn, vậy chạy thử xem sao
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}Kết quả ngoài dự liệu, lại bị crash, thông tin lỗi như sau, ý là không thể gọi phương thức đối với receiver nil, if closer != nil trong vòng lặp dường như không có tác dụng lọc,
panic: value method main.B.Close called using nil *B pointerVí dụ trên là phiên bản đơn giản hóa của bug mà tôi từng gặp phải, nhiều người mới bắt đầu có thể mắc phải lỗi này như tôi, dưới đây sẽ giải thích rốt cuộc là chuyện gì xảy ra.
Interface
Trong các chương trước đã đề cập, nil là giá trị zero của các kiểu tham chiếu, như slice, map, channel, hàm, con trỏ, interface. Đối với slice, map, channel, hàm, có thể coi chúng đều là con trỏ, đều do con trỏ trỏ đến phần triển khai cụ thể.

Nhưng chỉ có interface là không giống, interface được cấu thành từ hai thứ: kiểu và giá trị

Khi thử gán nil cho một biến, sẽ không thông qua biên dịch, và hiển thị thông tin như sau
use of untyped nil in assignmentNội dung đại khái là không thể khai báo một biến có giá trị là untyped nil. Đã có untyped nil, thì chắc chắn sẽ có typed nil, và tình huống này thường xuất hiện ở interface. Xem một ví dụ đơn giản dưới đây
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}Kết quả
<nil>
true
<nil>
falseKết quả rất kỳ lạ, rõ ràng output của pa là nil, nhưng nó lại không bằng nil, chúng ta có thể thông qua reflection để xem rốt cuộc nó là gì
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.TypeOf(pa))
fmt.Println(reflect.ValueOf(pa))
}Kết quả
<nil>
true
*int
<nil>Từ kết quả có thể thấy, nó thực tế là (*int)(nil), nghĩa là pa lưu trữ kiểu là *int, mà giá trị thực tế của nó là nil, khi thực hiện phép toán so sánh bằng đối với một giá trị kiểu interface, trước tiên sẽ phán đoán kiểu của chúng có bằng nhau không, nếu kiểu không bằng nhau thì trực tiếp判定 là không bằng nhau, sau đó mới phán đoán giá trị có bằng nhau không, logic phán đoán interface đoạn này có thể tham khảo từ hàm cmd/compile/internal/walk.walkCompare.
Vì vậy, nếu muốn một interface bằng nil, bắt buộc giá trị của nó phải là nil, và kiểu cũng phải là nil, vì kiểu trong interface thực tế cũng là một con trỏ
type iface struct {
tab *itab
data unsafe.Pointer
}Nếu muốn bỏ qua kiểu, trực tiếp phán đoán giá trị của nó có phải là nil không, có thể sử dụng reflection, dưới đây là một ví dụ
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}Thông qua IsNil() có thể trực tiếp phán đoán giá trị của nó có phải là nil không, như vậy sẽ không xảy ra vấn đề như ví dụ trên. Nên trong quá trình sử dụng hàng ngày, giả sử giá trị trả về của hàm là kiểu interface, nếu bạn muốn trả về một giá trị zero, tốt nhất trực tiếp trả về nil, không trả về bất kỳ giá trị zero của phần triển khai cụ thể nào, cho dù nó đã triển khai interface đó, nhưng nó永远 không thể bằng nil, điều này có thể dẫn đến lỗi như ví dụ.
Tóm tắt
Sau khi giải quyết vấn đề trên, tiếp theo xem mấy ví dụ dưới đây
Khi receiver của struct là receiver con trỏ, nil có thể sử dụng được, xem một ví dụ dưới đây
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}Đoạn code này có thể chạy bình thường, và không báo lỗi con trỏ rỗng.
Khi slice là nil, có thể truy cập độ dài và dung lượng của nó, cũng có thể thêm phần tử vào nó
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}Khi map là nil, vẫn có thể truy cập nó, nhưng map nil là chỉ đọc, một khi thử ghi sẽ gây ra panic
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// Khi thử ghi, sẽ gây ra panic
s["a"] = 1 // panic: assignment to entry in nil map
}Những đặc tính liên quan đến nil trong các ví dụ trên có thể khiến người ta bối rối, đặc biệt là đối với người mới học go, nil đại diện cho giá trị zero của mấy kiểu trên,也就是 giá trị mặc định, giá trị mặc định nên thể hiện hành vi mặc định, đây cũng chính là điều mà nhà thiết kế go muốn thấy: khiến nil trở nên hữu dụng hơn, thay vì trực tiếp ném ra lỗi con trỏ rỗng. Triết lý này cũng được thể hiện trong thư viện chuẩn, ví dụ khởi động một máy chủ HTTP có thể viết như sau
http.ListenAndServe(":8080", nil)Chúng ta có thể trực tiếp truyền vào một nil Handler, rồi thư viện http sẽ sử dụng Handler mặc định để xử lý yêu cầu HTTP.
TIP
Nếu quan tâm có thể xem video này Understanding nil - Gopher Conference 2016,讲解 rất rõ ràng dễ hiểu.
