Kesalahan Pointer nil
Pengantar
Dalam suatu proses penulisan kode, saya perlu memanggil metode Close() untuk menutup beberapa objek, seperti kode berikut
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
}Tetapi menulis begitu banyak判断 if terasa tidak elegan, B, C dan D semuanya mengimplementasikan metode Close, seharusnya bisa lebih ringkas, jadi saya memasukkannya ke dalam sebuah slice, kemudian loop untuk判断
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
}Ini terlihat似乎 lebih baik, mari jalankan dan lihat
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}Hasilnya di luar dugaan, ternyata crash, informasi error adalah sebagai berikut, artinya tidak dapat memanggil metode pada penerima nil, if closer != nil dalam loop sepertinya tidak起到 efek filtering,
panic: value method main.B.Close called using nil *B pointerContoh di atas adalah versi sederhana dari bug yang pernah saya temui, banyak pemula mungkin akan membuat kesalahan yang sama seperti saya, di bawah ini akan menjelaskan apa sebenarnya masalahnya.
Interface
Dalam bab sebelumnya disebutkan bahwa nil adalah nilai nol untuk tipe referensi, seperti slice, map, channel, fungsi, pointer, interface. Untuk slice, map, channel, fungsi, semuanya dapat dilihat sebagai pointer, semuanya ditunjuk oleh pointer ke implementasi spesifik.

Tetapi hanya interface yang berbeda, interface terdiri dari dua hal: tipe dan nilai

Ketika mencoba memberikan nilai nil ke sebuah variabel, tidak akan lolos kompilasi, dan akan menampilkan informasi berikut
use of untyped nil in assignmentKontennya kira-kira adalah tidak dapat mendeklarasikan variabel dengan nilai untyped nil. Karena ada untyped nil,相对的 pasti ada typed nil, dan situasi ini sering muncul pada interface. Lihat contoh sederhana berikut
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}Output
<nil>
true
<nil>
falseHasilnya sangat aneh, jelas output pa adalah nil, tetapi tidak sama dengan nil, kita dapat melihat apa sebenarnya melalui refleksi
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))
}Output
<nil>
true
*int
<nil>Dari hasil dapat dilihat, sebenarnya adalah (*int)(nil), artinya pa menyimpan tipe *int, sedangkan nilai sebenarnya adalah nil, ketika melakukan operasi perbandingan sama dengan nilai tipe interface, pertama akan判断 apakah tipenya sama, jika tipenya tidak sama, langsung dianggap tidak sama, kemudian baru判断 apakah nilainya sama, logika判断 interface bagian ini dapat merujuk ke fungsi cmd/compile/internal/walk.walkCompare.
Jadi, jika ingin sebuah interface sama dengan nil, harus nilainya nil, dan tipenya juga nil, karena tipe dalam interface sebenarnya juga sebuah pointer
type iface struct {
tab *itab
data unsafe.Pointer
}Jika ingin melewati tipe, langsung判断 apakah nilainya nil, dapat menggunakan refleksi, berikut adalah contohnya
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}Melalui IsNil() dapat langsung判断 apakah nilainya nil, dengan demikian tidak akan muncul masalah seperti di atas. Jadi dalam proses penggunaan sehari-hari, jika nilai return fungsi adalah tipe interface, jika ingin mengembalikan nilai nol, sebaiknya langsung return nil, jangan return nilai nol implementasi spesifik, meskipun mengimplementasikan interface tersebut, tetapi tidak akan pernah sama dengan nil, ini dapat menyebabkan kesalahan seperti dalam contoh.
Ringkasan
Setelah menyelesaikan masalah di atas, selanjutnya lihat beberapa contoh berikut
Ketika penerima metode struktur adalah penerima pointer, nil dapat digunakan, lihat contoh berikut
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}Kode ini dapat berjalan normal, dan tidak akan melaporkan error pointer null.
Ketika slice adalah nil, dapat mengakses panjang dan kapasitasnya, juga dapat menambahkan elemen kepadanya
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}Ketika map adalah nil, masih dapat mengaksesnya, tetapi map nil adalah read-only, sekali mencoba menulis akan memicu panic
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// Mencoba menulis, akan memicu panic
s["a"] = 1 // panic: assignment to entry in nil map
}Karakteristik nil dalam contoh di atas mungkin membingungkan, terutama bagi pemula Go, nil mewakili nilai nol dari beberapa tipe di atas, yaitu nilai default, nilai default seharusnya menunjukkan perilaku default, ini juga yang diinginkan oleh desainer Go: membuat nil lebih berguna, bukan langsung melempar error pointer null. Filosofi ini juga tercermin dalam pustaka standar, misalnya memulai server HTTP dapat ditulis seperti ini
http.ListenAndServe(":8080", nil)Kita dapat langsung memasukkan nil Handler, kemudian pustaka http akan menggunakan Handler default untuk menangani permintaan HTTP.
TIP
Yang tertarik dapat menonton video ini Understanding nil - Gopher Conference 2016, penjelasannya sangat jelas dan mudah dipahami.
