nil İşaretçi Hatası
Giriş
Bir kodlama oturumu sırasında, birden fazla nesneyi kapatmak için Close() metodunu çağırmam gerekti, aşağıdaki kodda olduğu gibi:
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
}Ancak bu kadar çok if ifadesi yazmak zarif gelmedi. B, C ve D'nin hepsi Close metodunu uyguladığı için, daha öz olabileceğini düşündüm. Bu yüzden onları bir dilime koydum ve döngü kullandım:
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
}Bu daha iyi görünüyordu, şimdi çalıştıralım ve görelim:
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}Sonuç beklenmedik oldu—gerçekten çöktü. Hata mesajı nil alıcı üzerinde bir metodun çağrılamayacağını gösteriyordu. Döngüdeki if closer != nil ifadesinin filtreleme etkisi yok gibiydi:
panic: value method main.B.Close called using nil *B pointerYukarıdaki örnek karşılaştığım bir hatanın basitleştirilmiş versiyonudur. Birçok başlangıç seviyesindeki geliştirici ilk başta bu tür bir hata yapabilir. Neler olduğunu açıklayayım.
Interface
Önceki bölümlerde, nil'in referans tipleri için sıfır değer olduğu belirtilmiştir, örneğin dilimler, map'ler, kanallar, fonksiyonlar, işaretçiler ve interface'ler. Dilimler, map'ler, kanallar ve fonksiyonlar için, hepsi somut uygulamalara işaret eden işaretçiler olarak görülebilir.

Ancak interface'ler farklıdır. Bir interface iki şeyden oluşur: bir tip ve bir değer.

nil'i bir değişkene atamaya çalıştığınızda, derlenmez ve aşağıdaki mesajı gösterir:
use of untyped nil in assignmentİçerik kabaca untyped nil değerine sahip bir değişkenin bildirilemeyeceğini gösterir. untyped nil olduğuna göre, typed nil de olmalıdır ve bu durum genellikle interface'lerle olur. Basit bir örneği düşünün:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}Çıktı:
<nil>
true
<nil>
falseSonuç çok garip. Açıkça pa nil çıktısı veriyor, ancak nil'e eşit değil. Ne olduğunu görmek için yansıma kullanabiliriz:
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))
}Çıktı:
<nil>
true
*int
<nil>Sonuçtan, bunun aslında (*int)(nil) olduğunu görebiliriz. Bu, pa'nın *int tipini sakladığı, gerçek değerinin ise nil olduğu anlamına gelir. Interface değerleri üzerinde eşitlik karşılaştırması yaparken, önce tiplerinin eşit olup olmadığına bakar. Tipler eşit değilse, doğrudan eşit olmadığını belirler, sonra değerlerin eşit olup olmadığına bakar. Bu interface karşılaştırma mantığı cmd/compile/internal/walk.walkCompare fonksiyonunda referans alınabilir.
Bu nedenle, bir interface'in nil'e eşit olmasını istiyorsanız, hem değeri hem de tipi nil olmalıdır. Çünkü interface'deki tip aslında bir işaretçidir:
type iface struct {
tab *itab
data unsafe.Pointer
}Tipi atlayıp doğrudan değerinin nil olup olmadığını kontrol etmek istiyorsanız, yansıma kullanabilirsiniz. İşte bir örnek:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}IsNil() kullanarak, doğrudan değerinin nil olup olmadığını kontrol edebilirsiniz, yukarıda belirtilen problemi önlersiniz. Yani günlük kullanımda, eğer bir fonksiyonun dönüş değeri interface tipi ise ve sıfır değer döndürmek istiyorsanız, herhangi bir somut uygulamanın sıfır değerini değil, doğrudan nil döndürmek en iyisidir. Interface'i uygulası bile, asla nil'e eşit olmayacaktır, bu da örnekteki hataya yol açabilir.
Özet
Yukarıdaki problemi çözdükten sonra, aşağıdaki örneklere bakalım:
Bir struct'ın alıcısı işaretçi alıcısı olduğunda, nil kullanılabilir. Bu örneği düşünün:
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}Bu kod normal şekilde çalışır, null işaretçi hatası olmadan.
Bir dilim nil olduğunda, uzunluğuna ve kapasitesine erişebilirsiniz ve ona elemanlar ekleyebilirsiniz:
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}Bir map nil olduğunda, yine de ona erişebilirsiniz, ancak nil bir map salt okunurdur. Yazmaya çalışmak panic'e neden olur:
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// Yazmaya çalışmak panic'e neden olur
s["a"] = 1 // panic: assignment to entry in nil map
}Yukarıdaki örneklerdeki bu nil özellikleri kafa karıştırıcı olabilir, özellikle Go başlangıç seviyesindeki geliştiriciler için. nil yukarıda belirtilen tipler için sıfır değeri temsil eder, bu varsayılan değerdir. Varsayılan değerler varsayılan davranış sergilemelidir, bu tam olarak Go'nun tasarımcılarının amaçladığı şeydir: nil'i daha kullanışlı yapmak, hemen null işaretçi hataları vermek yerine. Bu felsefe standart kütüphanede de yansıtılır. Örneğin, bir HTTP sunucusu başlatmak şöyle yazılabilir:
http.ListenAndServe(":8080", nil)Doğrudan nil Handler geçebiliriz ve http kütüphanesi HTTP isteklerini işlemek için varsayılan Handler'ı kullanır.
TIP
Eğer ilgileniyorsanız, bu videoyu izleyebilirsiniz Understanding nil - Gopher Conference 2016, çok açık ve anlaşılması kolay.
