İçeriğe geç
academia.sh

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.errorStringerrors.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 error değeridir; kimliği metninde değil, kendi değerinde durur.
  • errors.Newin her çağrısı yeni bir *errors.errorString değeri üretir — aynı metin bile versin, iki çağrı iki farklı değer, dolayısıyla iki farklı kimlik demektir.
  • Sarmalanmamış bir hatada errors.Is ile == 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) bool yöntemine de dayanabilir; errors.Is ikisini 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat