Go dilinde, bir arayüz içsel olarak bir tür işaretçisi ve bir değer işaretçisinden oluşan iki kelimelik bir yapı olarak uygulanır. Gerçekten nil olan bir arayüz hem alanları nil olarak ayarlanmışken, nil somut değeri tutan bir arayüz, tür alanı somut tür bilgisi ile doldurulmuş ancak değer alanı nil’e işaret eden bir yapıdadır. Bu ayrım, alttaki somut değer nil olsa bile arayüz'ün nil olmadığını, çünkü tür meta verilerini taşıdığını ifade eder. Bir arayüz nil ile karşılaştırıldığında, Go çiftin her iki kelimesini kontrol eder, bu da türü nil olan bir değerin, altta yatan işaretçi sıfır olmasına rağmen, eşitlik karşılaştırmalarında nil olmayan bir ifade olarak değerlendirilmesine neden olur.
Bu sorunlu kodu düşünün:
type MyError struct { msg string } func (*MyError) Error() string { return "error" } func DoWork() error { var err *MyError = nil return err // *MyError türü ile birlikte, değeri nil olan arayüzü döner } func main() { if err := DoWork(); err != nil { fmt.Println("Başarısız") // "Başarısız!" yazdırır! } }
Burada, err nil değildir çünkü arayüz tür bilgisi içerir.
Bu sorunu, yüksek verimli bir REST API hizmeti oluştururken, veritabanı soyutlama katmanımızın özel bir *DbError yapısını bir hata arayüzü olarak döndürdüğünde yaşadık. Veritabanı işlevi, hata meydana gelmediğinde nil dönerken, HTTP ara yazılımımızın standart if err != nil kontrolü sürekli olarak hata kaydını tetikler ve başarılı isteklerde bile HTTP 500 durum kodlarını döndürüyordu. Bu, çağrı yığınında iz sürerken durumun, önce yarış koşulları veya veritabanı sürücü hataları beklediğimiz bir haftalık hata ayıklama seansına yol açtı, sonra hata değişkeninin nil işaretçisini içeren bir nil olmayan arayüz tuttuğunu fark ettik.
Düşündüğümüz bir çözüm, her dönüş ifadesini açıkça somut işaretçiyi hata arayüzü'ne dönüştürmekti, örneğin return error((*DbError)(nil)), nil yazarak, ancak bu yaklaşım hala nil işaretçisini tür bilgisi doldurulmuş bir arayüz içine sarıyordu ve nil olmayan arayüz durumunu koruyordu, eşitlik kontrolünü başarısızlıkla sonuçlandırıyordu. Bu desen, aynı zamanda hataya açık ve geliştiricilerin sistemdeki her hata dönüş yolunda belirli bir formülü hatırlamasını gerektiren ayrıntılı, tekrarlayan kod oluşturdu. Başka bir yaklaşım, DbError türümüze özel bir IsNil() yöntemini eklemek ve tüm çağırıcıların bu yöntemi standart nil karşılaştırmasından önce kontrol etmesini zorunlu kılmaktı, ancak bu, standart Go hata işleme desenleri ile tutarsızlık yaratıyordu ve her tüketen paketin özel hata uygulamamızı içe aktarmasını ve anlamasını gerektiriyordu.
Sonunda, iç işlevlerden somut işaretçiyi doğrudan döndürmeyi ve yalnızca gerçekte nil olmadığında arayüz içine sarmayı seçtik, API sınırında if dbErr != nil { return dbErr, nil } else { return nil, nil } gibi açık bir kontrol kullanarak. Bu yaklaşım, tüm çağrı yerlerinde deyimsel hata kontrolünü korurken, tür-nil belirsizliğini tamamen ortadan kaldırdı ve iç hata işleme için derleme zamanı tür güvenliğini korumamıza olanak sağladı. Düzeltme, hayalet hata kaydı sorunlarını hemen çözmüş, başarılı veritabanı işlemleri için beklenen HTTP 200 yanıtlarını geri getirmiş ve mikro hizmetlerimizde arayüz nil karşılaştırmalarıyla ilgili potansiyel hata sınıfını ortadan kaldırmıştır.
Bir nil arayüz üzerinde bir yöntem çağırmanın her zaman panik yaratmasının sebebi nedir, oysa arayüz içindeki biçimlendirilmiş nil değerine sahip bir değerin başarılı olabileceği?
Gerçekten nil olan bir arayüz tuttuğunuzda, tür ve değer kelimeleri boş olduğunda, hangi yöntem uygulamasını dağıtmak için hangi tür bilgilerin mevcut olduğu belirlenemez, bu da hemen bir çalışma zamanı panikletmesine yol açar. Ancak, somut işaretçi nil olduğunda fakat arayüz tür bilgisi taşıdığında, Go tam olarak hangi yöntem uygulamasını çağıracağını biliyor ve alıcı nil işaretçilerini güvenli bir şekilde işlerse, yöntem çalıştırma normal bir şekilde devam eder. Bu ayrımı anlamak, yöntemlerin nil alıcılar için açıkça kontrol edilmesi gerektiği yerlerde sağlam API tasarımları uygulamak için kritik öneme sahiptir.
reflect paketi IsNil yöntemi, bir arayüz değeri ile bir somut işaretçiyi kontrol ettiğinde neden farklı davranır?
reflect paketinin Value.IsNil yöntemi, nil bir arayüz değeri üzerinde çağrıldığında panik yapar çünkü nil olduğunu sorgulamak için somut bir tür mevcut değildir, oysa bir tür nil değerini tutan bir arayüz için panik yapmadan doğru döner. Adaylar genellikle reflect.ValueOf(x).IsNil'in evrensel bir nil kontrolü sağladığını varsayar, ancak bu, altta yatan değerin bir kanal, işlev, arayüz, harita, işaretçi veya dilim olması gerektiği ve arayüz değerinin nil olması ile nil işaretçisini içermesi durumunda farklı davrandığını gerektirir. Bu ince ayrıntı, reflect'in önce arayüz'ü açtığını ve somut değere eriştiğini anlamayı gerektirir, bu da birçok geliştiriciyi genel hata ayıklama yardımcı programlarını yazarken hazırlıksız yakalar.
Nil somut işaretçiye sahip bir arayüz üzerinde tür belirleme işlemi neden başarılı olur ve panik yaratmaz, ve bu, temel veri yapısı hakkında neyi gösterir?
Bir tip belirleme işlemi gibi v := err.(*MyError) bir arayüz üzerinde nil somut işaretçiyi tutuyorsa, bu işlem başarılı olur ve nil işaretçisi döner; panik yapmaz veya iki değerli biçimde false döndürmez, çünkü arayüz hala geçerli tür bilgisi taşır. Bu, Go dilinin arayüzleri tür-değer çiftleri olarak uyguladığını gösterir; burada, tip belirlemenin geçerliliği yalnızca saklanan türün belirtilen türle atanabilir olup olmadığına bağlıdır, tamamen değer işaretçisinin nil olup olmamasıyla bağımsızdır. Adaylar genellikle başarılı bir belirlemeden sonra v == nil ifadesinin işaretçi değeri ile karşılaştırıldığında doğru değerlendirilebileceğini, ancak orijinal arayüz err == nil ifadesinin hâlâ false kaldığını gözden kaçırır, bu da hata çıkarma ve tip değiştirme kodunda ince mantık hatalarına yol açar.