Skip to content

Kesalahan Pointer nil

Pengantar

Dalam suatu proses penulisan kode, saya perlu memanggil metode Close() untuk menutup beberapa objek, seperti kode berikut

go
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判断

go
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

go
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 pointer

Contoh 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 assignment

Kontennya 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

go
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>
false

Hasilnya sangat aneh, jelas output pa adalah nil, tetapi tidak sama dengan nil, kita dapat melihat apa sebenarnya melalui refleksi

go
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

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

Jika ingin melewati tipe, langsung判断 apakah nilainya nil, dapat menggunakan refleksi, berikut adalah contohnya

go
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

go
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

go
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

go
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.

Golang by www.golangdev.cn edit