Ders 07 / 13
Nöbetçi Hatalar
Paket düzeyinde bir kez tanımlanan bir hata değerinin kimliği metninde değil kendisinde durur: aynı metni taşıyan başka bir errors.New değeri onunla karşılaştırıldığında eşit çıkmaz, çünkü ikisi de errors.errorString tipinde farklı iki değerdir. errors.Is, aynı sonucu == ile aynı verir. Sınırlayıcı ölçüm: nöbetçi hatanın kimliği değiştiğinde çağıran taraf sessizce yanlış dala düşer, hiçbir uyarı üretilmez.
İçindekiler
Önceki ders, değer alıcılı bir hatanın yapısal, işaretçi alıcılı bir hatanın kimlikle karşılaştırıldığını ölçtü. Bu ders o farkı bir adım ileri taşıyor: bir çağıran, belirli bir hatayı tanımak istediğinde neye güveniyor? Yanıt Go’da yaygın bir kalıba dayanıyor — nöbetçi hata: paket düzeyinde bir kez tanımlanan, dışa aktarılan bir hata değeri. Bu dersin sorusu şu: bu değerin kimliği nerede duruyor, ve aynı metni taşıyan başka bir hata onunla neden karıştırılmıyor?
Bu isim, Algoritmalar kursundaki nöbetçi değişkeni ve Veri Yapıları kursundaki nöbetçi değer ile aynı aileden geliyor: orada bir dizinin sonunu ya da özel bir konumu işaretleyen tek bir değer, burada bir başarısızlık durumunu işaretliyor. Kavram tekrar kurulmuyor; ortak olan yalnızca fikir — bir arama ya da denetim döngüsünü, her adımda ayrı bir kural yazmak yerine tek bir karşılaştırmaya indirgeyen özel bir değer.
Aynı Metin, Ayrı Kimlik
errors.New, çağrıldığı her seferde yeni bir değer üretir. Aşağıdaki araBirinci paket
düzeyinde tanımlanan ErrBulunamadi nöbetçisini döndürüyor; araIkinci ise aynı metni taşıyan
ama errors.New ile ayrıca üretilmiş bir değer döndürüyor. İkisi de kullanıcıya aynı
şeyi söylüyormuş gibi görünüyor.
Bu ikisi gerçek kodda genellikle iki ayrı paketten geliyor: bir depo katmanı ErrBulunamadi
gibi bir nöbetçi tanımlar, çağıran bu nöbetçiyle karşılaştırarak “kayıt yoktu” durumunu özel
olarak ele alır. Sorun, bir başka katmanın — belki aynı anlamı taşımak isteyen bir üçüncü
paketin — kendi errors.New("kayit bulunamadi") çağrısını yazmasıyla başlıyor; metin
okuyucuya aynı görünse de, iki değer birbirinden habersiz iki ayrı kimlik taşıyor.
// bag.go — hata bagi kullanim yerinde mi kuruluyor, degerin icinde mi
package main
import "fmt"
var kullanimYeri, deger int
func gozle(ad, yer, kanit string) {
switch yer {
case "kullanim":
kullanimYeri++
default:
deger++
}
fmt.Printf("%-42s %-9s %s\n", ad, yer, kanit)
}
func ozet() {
fmt.Printf("bag: %d kullanim yeri, %d deger\n", kullanimYeri, deger)
}
// nobetci.go — paket duzeyinde tanimlanan bir hata degeri, ayni metinli baska bir degerden kimlikle ayrilir
package main
import (
"errors"
"fmt"
)
var ErrBulunamadi = errors.New("kayit bulunamadi")
func araBirinci(id int) error {
if id != 1 {
return ErrBulunamadi
}
return nil
}
func araIkinci(id int) error {
if id != 1 {
return errors.New("kayit bulunamadi")
}
return nil
}
func main() {
h1 := araBirinci(9)
h2 := araIkinci(9)
gozle("iki degerin dinamik tipi", "deger", fmt.Sprintf("%T, %T", h1, h2))
gozle("nobetciyle == karsilastirmasi", "deger", fmt.Sprint(h1 == ErrBulunamadi))
gozle("nobetciyle errors.Is karsilastirmasi", "deger", fmt.Sprint(errors.Is(h1, ErrBulunamadi)))
gozle("ayni metinli farkli degerin == sonucu", "deger", fmt.Sprint(h2 == ErrBulunamadi))
gozle("ayni metinli farkli degerin errors.Is sonucu", "deger", fmt.Sprint(errors.Is(h2, ErrBulunamadi)))
gozle("iki farkli degerin metin karsilastirmasi", "deger", fmt.Sprint(h1.Error() == h2.Error()))
ozet()
}
iki degerin dinamik tipi deger *errors.errorString, *errors.errorString nobetciyle == karsilastirmasi deger true nobetciyle errors.Is karsilastirmasi deger true ayni metinli farkli degerin == sonucu deger false ayni metinli farkli degerin errors.Is sonucu deger false iki farkli degerin metin karsilastirmasi deger true bag: 0 kullanim yeri, 6 deger
HT7. İlk satır önceki dersin dinamik tip ölçümünü burada tekrarlıyor ve şaşırtıcı bir
sonuç veriyor: h1 ve h2 aynı dinamik tipi taşıyor, *errors.errorString — errors.New
standart kitaplıkta bu tek tipin bir işaretçisini döndürüyor, hangi metni verirsen ver. Yani
aynı tip olmak, aynı değer olmak anlamına gelmiyor. h1 == ErrBulunamadi true dönüyor çünkü
araBirinci gerçekten paket düzeyindeki aynı değişkeni döndürdü — aynı adresi taşıyan aynı
işaretçi. h2 == ErrBulunamadi ise false dönüyor, çünkü araIkinci kendi errors.New
çağrısıyla yeni bir değer üretti; metin birebir aynı olsa da bu, farklı bir bellek
konumunda duran farklı bir yapı demek.
HT8. errors.Is(h1, ErrBulunamadi) ve errors.Is(h2, ErrBulunamadi), sarmalanmamış bu iki
hatada == ile birebir aynı sonucu veriyor. Bunun nedeni errors.Isin davranışı: hedef hata
sarmalanmadıysa, karşılaştırma doğrudan ==e iniyor. Bu eşdeğerlik yalnız bu derste geçerli —
sıradaki ders bir hata sarmalandığında ==in neden yetersiz kaldığını ve errors.Isin neden
zorunlu hâle geldiğini ölçüyor. Son satır ise metin karşılaştırmasının (Error() == Error())
kimlik karşılaştırmasından tamamen ayrı bir soruya yanıt verdiğini gösteriyor: iki farklı
değer, metni birebir aynı olduğu için true dönüyor — kimlik sorusuna yanlış yanıt verecek
bir kısayol.
Bu ölçüm, nöbetçi hata kalıbının neden const değil var ile kurulduğunu da açıklıyor.
error bir arayüz olduğu için bir sabite bağlanamaz — sabitler yalnız temel tiplerde
(sayı, dizgi, bool) tanımlanabilir. Bir nöbetçi hatanın “sabit” gibi davranması, dilin bir
kısıtlamasından değil, paket düzeyinde bir kez oluşturulup bir daha hiç değiştirilmeyen tek bir
değişkene bütün paketin aynı işaretçiyle erişmesinden geliyor. Kimliğin korunması, bu tek
değişkenin paylaşılmasına bağlı; her çağıran kendi errors.New kopyasını üretirse, kimlik
karşılaştırması daha en baştan anlamını yitiriyor.
Özel Kimlik: Is Yöntemiyle Kurulan Karşılaştırma
errors.Isin bu derste ölçülen davranışı — sarmalanmamış bir hatada ==e inmesi — tek
kuralı değil. errors paketi bir hataya, karşılaştırmayı kendi belirlediği bir yönteme
devretme imkânı veriyor: Is(hedef error) bool yazan bir tip, errors.Is tarafından
çağrıldığında işaretçi eşitliğine değil, kendi mantığına göre yanıt veriyor. Bu, kimliğin
her zaman “aynı adres” anlamına gelmediğini, bir tipin kendi kimlik tanımını yazabildiğini
gösteriyor.
// kodhatasi.go — ozel Is yontemiyle kurulan bir kimlik: isaretci esitligi olmadan da kimlik tanimlanabilir
package main
import (
"errors"
"fmt"
)
type KodHatasi struct{ Kod int }
func (k *KodHatasi) Error() string { return fmt.Sprintf("hata kodu %d", k.Kod) }
func (k *KodHatasi) Is(hedef error) bool {
h, tamam := hedef.(*KodHatasi)
return tamam && h.Kod == k.Kod
}
func main() {
a := &KodHatasi{Kod: 404}
b := &KodHatasi{Kod: 404}
c := &KodHatasi{Kod: 500}
gozle("iki ayri isaretcinin == sonucu", "deger", fmt.Sprint(a == b))
gozle("Is yontemiyle ayni kodun karsilastirmasi", "deger", fmt.Sprint(errors.Is(a, b)))
gozle("Is yontemiyle farkli kodun karsilastirmasi", "deger", fmt.Sprint(errors.Is(a, c)))
ozet()
}
iki ayri isaretcinin == sonucu deger false Is yontemiyle ayni kodun karsilastirmasi deger true Is yontemiyle farkli kodun karsilastirmasi deger false bag: 0 kullanim yeri, 3 deger
HT9. a ve b, KodHatasi tipinden iki ayrı işaretçi; == bunları adres bazında
karşılaştırıyor ve false dönüyor, tıpkı önceki dersteki ErisimHatasi örneğinde olduğu gibi.
Ama errors.Is(a, b) çağrıldığında sonuç true — çünkü KodHatasi, Is(hedef error) bool
yöntemini yazmış ve errors.Is, hedefi anın Is yöntemine soruyor. a.Is(b), bnin de bir
*KodHatasi olduğunu ve Kod alanının eşit olduğunu doğruluyor; adres hiç devreye girmiyor.
HT10. errors.Is(a, c) ise false dönüyor, çünkü cnin Kod alanı farklı — özel Is
yöntemi hâlâ çalışıyor, yalnız bu kez kendi mantığına göre “eşit değil” diyor. Bu, kimliğin
Go’da tek bir mekanizmaya bağlı olmadığını gösteriyor: paket düzeyinde paylaşılan bir
değişkenin adresi bir kimlik biçimiyse, bir tipin kendi yazdığı Is yöntemi de başka bir
kimlik biçimi. errors.Is ikisini de kabul ediyor — önce == dener, sonra hedefin ya da
kaynağın bir Is yöntemi olup olmadığına bakıyor. Sıradaki bölümdeki nöbetçi hata ölçümü ilk
biçime, yani paylaşılan değişkene dayanıyor.
Bu iki biçimin seçimi, hatanın taşıdığı bilgiye bağlı. ErrBulunamadi gibi hiçbir ek veri
taşımayan bir durumda tek bir paylaşılan değer yeterli — her çağıran aynı adrese bakabiliyor.
Ama KodHatasi gibi kendi içinde değişen bir veri (Kod) taşıyan bir hata türünde, olası her
kod için önceden ayrı bir paylaşılan değişken tanımlamak pratik değil; Is yöntemi, “aynı
tipten ve aynı koddan mı” sorusunu tek bir karşılaştırmaya indiriyor ve her kod için yeni bir
nöbetçi üretme ihtiyacını ortadan kaldırıyor.
Sınırlayıcı Ölçüm: Kimlik Bir API Sözüdür
Bir nöbetçi hata dışa aktarıldığında (ErrBulunamadi gibi büyük harfle başlayan bir ad),
paketin kendisi bir söz veriyor: bu değer, bu paketten dönen belirli bir hata durumunu her
zaman aynı işaretçiyle temsil edecek. Bu söz bozulduğunda — aynı durumu farklı bir
errors.New çağrısıyla üreten bir değişiklik yapıldığında — kaynakta hiçbir uyarı çıkmıyor.
Bu söz genellikle kod tabanı büyüdükçe bozuluyor, kötü niyetten değil dikkatsizlikten. Bir
paketin ilk sürümü tek bir yerden ErrYetkisiz döndürürken, zamanla aynı durumu bildiren
ikinci bir işlev eklenir; yazan kişi paket düzeyindeki nöbetçiyi içe aktarmak yerine kendi
errors.New çağrısını yazarsa — belki dosyalar arasında geçiş yaparken, belki nöbetçinin
varlığından habersiz olduğu için — iki işlev artık aynı metni ama farklı kimliği taşıyan iki
ayrı hata üretir. Aşağıdaki ölçüm bu senaryoyu birebir kuruyor: erisimA nöbetçiyi
paylaşıyor, erisimB paylaşmıyor.
// versiyon.go — nobetci hatanin kimligi degisirse cagiran sessizce yanlis dala duser
package main
import "errors"
var ErrYetkisiz = errors.New("yetkisiz erisim")
func erisimA(rutbe int) error {
if rutbe < 2 {
return ErrYetkisiz
}
return nil
}
func erisimB(rutbe int) error {
if rutbe < 2 {
return errors.New("yetkisiz erisim")
}
return nil
}
func isle(hata error) string {
if hata == ErrYetkisiz {
return "yetkisizlik dalina dusuldu, ozel islem yapildi"
}
if hata != nil {
return "bilinmeyen hata dalina dusuldu, genel islem yapildi"
}
return "hata yok"
}
func main() {
gozle("erisimA'nin nobetci hatasiyla isle", "deger", isle(erisimA(0)))
gozle("erisimB'nin ayni metinli hatasiyla isle", "deger", isle(erisimB(0)))
ozet()
}
erisimA'nin nobetci hatasiyla isle deger yetkisizlik dalina dusuldu, ozel islem yapildi erisimB'nin ayni metinli hatasiyla isle deger bilinmeyen hata dalina dusuldu, genel islem yapildi bag: 0 kullanim yeri, 2 deger
HT11. erisimA ve erisimB, kullanıcı için görünüşte aynı durumu bildiriyor — “yetkisiz
erişim” — ama yalnız erisimA gerçek ErrYetkisiz nöbetçisini döndürüyor. isle işlevi
hata == ErrYetkisiz ile özel bir dal ayırmış: erisimAnın hatası bu dala düşüyor, erisimB
ise düşmüyor ve genel dala kayıyor. Kaynağı okuyan biri için erisimBnin metni erisimAnınkiyle
birebir aynı olduğundan, bu ayrımın var olduğunu görmek mümkün değil — yalnız çalıştırıp
sonuçları karşılaştırmak gösteriyor.
HT12. Bu, nöbetçi hatanın kimliğinin neden bir API sözü sayıldığını gösteriyor:
erisimB gibi bir işlev, aynı durumu bildirdiğini düşünse de nöbetçiyi paylaşmadığı için sözü
tutmuyor. Derleme hiçbir şey söylemiyor, çünkü ikisi de geçerli bir error döndürüyor;
çalışma zamanında da hiçbir panik ya da hata mesajı çıkmıyor — isle yalnız beklenmeyen dala
sessizce düşüyor. Bir paketin bir nöbetçi hatayı bir kez yayımladıktan sonra, o hatayı üreten
her yol aynı değişkeni döndürmek zorunda; aksi hâlde çağıran tarafın kimlik karşılaştırması
gizlice yanlış sonuç vermeye başlıyor.
Bunun bir sınırı da var: kimlik yalnızca aynı çalışan süreç içinde anlamlı. ErrYetkisizin
adresi, programın o çalıştırılışına özgü; bir hatayı diske yazıp sonra okumak, ya da bir ağ
çağrısıyla başka bir sürece taşımak, error değerini değil onun Error() metnini taşıyor —
kimlik bu geçişte kayboluyor. Bu yüzden nöbetçi hata karşılaştırması yalnızca aynı ikili
dosyanın (aynı derlemenin) içindeki çağrılar arasında güvenilir; sınır ötesinde (bir HTTP
yanıtında, bir günlük dosyasında) hata metninin ya da ayrı bir hata kodu alanının taşınması
gerekiyor.
Bu derste ölçülen üç bulgu birleştiğinde konunun ölçütü hata değerlerinde ilk kez tümüyle
değer sınıfına oturuyor. Örtük karşılama önceki dersin de gösterdiği gibi kullanım yerinde
kuruluyordu; ama bir hatayı tanımak, yani onu başka bir hatadan ayırt etmek, tamamen
değerin kendi kimliğine dayanıyor — ne kaynakta bir bildirim, ne derleyicinin baktığı bir
denetim var. Kimlik ya paylaşılan bir adrese (ErrBulunamadi) ya da tipin kendi yazdığı bir
Is yöntemine dayanıyor; ikisi de yalnızca çalışma zamanında, değere sorularak öğreniliyor.
Özet
- Bir nöbetçi hata, paket düzeyinde bir kez tanımlanan ve dışa aktarılan bir
errordeğeridir; kimliği metninde değil, kendi değerinde durur. errors.Newin her çağrısı yeni bir*errors.errorStringdeğeri üretir — aynı metin bile versin, iki çağrı iki farklı değer, dolayısıyla iki farklı kimlik demektir.- Sarmalanmamış bir hatada
errors.Isile==aynı sonucu verir; bu eşdeğerlik yalnız hata hiç sarmalanmadığında geçerlidir. - Metin karşılaştırması (
Error() == Error()) kimlik karşılaştırmasının yerini tutmaz: farklı kimlikli iki hata birebir aynı metni taşıyabilir. - Nöbetçi hatanın kimliği bir API sözüdür; bir durumu bildiren her yol aynı paylaşılan değişkeni döndürmezse, çağıranın kimlik denetimi hiçbir uyarı vermeden yanlış sonuç üretir.
- Kimlik yalnız paylaşılan bir adrese değil, bir tipin kendi yazdığı
Is(hedef error) boolyöntemine de dayanabilir;errors.Isikisini de tanır,==yalnız birincisini. Kimlik yalnızca aynı çalışan süreç içinde geçerlidir, bir sınırı (diske yazma, ağ üzerinden taşıma) aşarken kaybolur.
Sonraki Adım
Bu derste ölçülen hatalar hep tek katmanlıydı: bir işlev nöbetçiyi ya doğrudan döndürdü ya da
onun yerine yeni bir değer üretti. Ama gerçek çağrı zincirleri genellikle birden çok katmandan
geçiyor ve her katman kendi bağlamını eklemek istiyor — “şu işlemi yaparken böyle bir hata
oluştu” gibi. Bir hata bu şekilde sarmalandığında, h1 == ErrBulunamadi artık işe yaramıyor;
dıştaki değer artık nöbetçinin kendisi değil, onu içinde taşıyan başka bir değer. errors.Isin
bu derste sarmalanmamış hatalarda ==e indiği gözlemi de burada sona eriyor: sıradaki derste
zincir eklendiğinde, errors.Isin neden artık ==den farklı bir sonuç ürettiği ve zincirin
dibindeki nöbetçinin kaç katman sonra hâlâ bulunabildiği ölçülüyor. Aynı soru, sarmalayan
metnin kendisi için de sorulacak: bir katmanın eklediği bağlam, zincirin geri kalanında nasıl
korunuyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.