Ders 09 / 13
Panik ve Kurtarma
Bir işlev hata dönmek yerine panikleyebilir ve bu seçim imzada hiçbir iz bırakmaz: (int, error) döndüren ve yalnız int döndüren iki işlev, biri her zaman değer üretirken öbürü çalışma zamanında çökebilir. recover yalnız kendisini doğrudan çağıran ertelenmiş işlevde çalışır; kurtarılan bir panik hataya çevrilmezse bilgi tümüyle kaybolur. Yığıt izi basılmaz, yalnız panik=true/false ve kurtarılan değerin tipi ölçülür.
İçindekiler
Önceki üç ders, bir işlevin başarısızlığını her zaman aynı sözleşmeyle bildirdiğini ölçtü:
son dönüş değeri bir error. Zincir ne kadar uzarsa uzasın, katman ne kadar bağlam eklerse
eklesin, imza hep aynı kaldı. Bu ders o sözleşmenin dışına çıkıyor: Go’da bir işlevin
başarısız olmasının imzada hiçbir iz bırakmayan bir yolu var — panik. Bu dersin sorusu, konunun
sorusunun en keskin biçimi: paniğin bağı hiç kurulmuyorsa, çağıran bunu önceden nasıl bilebilir?
Panik, dilin çalışma zamanının kendisinin de kullandığı bir mekanizma: bir dilim sınırının
dışına erişmek, bir sözlüğe yazmadan önce ilklendirilmemiş bir işaretçiden okumak, sıfıra
bölme yapmak — bunların hepsi çalışma zamanı tarafından üretilen birer panik. Bu ders yalnız
programcının kendi yazdığı panic çağrılarını ölçüyor, ama gözlem ikisi için de aynı: hiçbiri
imzada görünmüyor.
İmzada Görünen Kanal, Görünmeyen Çıkış
Aynı işi iki biçimde yazmak mümkün: bir Depo türünün getir işlevi ya (int, error)
döndürüp bulunamama durumunu ikinci değerde taşıyabilir, ya da yalnız int döndürüp
bulunamadığında panikleyebilir. Aşağıdaki ölçüm ikisini de üç katmanlı bir çağrı zincirinde
kuruyor — ic, onu çağıran orta, onu çağıran dis. Üç katman bilerek seçildi: Hata
Sarmalama ve Çözme dersindeki zincirle aynı derinlik, ama bu kez katmanlar hiçbir bağlam
eklemiyor, yalnızca çağrıyı iletiyor — ölçülen şey bağlamın taşınması değil, başarısızlık
kanalının kendisinin katmanlar boyunca ne kadar görünür kaldığı.
// bag.go — panigin bagi bildirimde mi, degerin icinde mi, yoksa hicbir yerde mi kuruluyor
package main
import "fmt"
var bildirim, deger, yok int
func gozle(ad, yer, kanit string) {
switch yer {
case "bildirim":
bildirim++
case "deger":
deger++
default:
yok++
}
fmt.Printf("%-46s %-9s %s\n", ad, yer, kanit)
}
func ozet() {
fmt.Printf("bag: %d bildirim, %d deger, %d hicbir yerde\n", bildirim, deger, yok)
}
// bicimler.go — ayni is iki bicimde: hata donen ve panikleyen, uc katmanli zincir
package main
import "fmt"
type Depo struct{ ogeler map[string]int }
func (d Depo) icHata(ad string) (int, error) {
v, tamam := d.ogeler[ad]
if !tamam {
return 0, fmt.Errorf("bulunamadi: %s", ad)
}
return v, nil
}
func (d Depo) ortaHata(ad string) (int, error) { return d.icHata(ad) }
func (d Depo) disHata(ad string) (int, error) { return d.ortaHata(ad) }
func (d Depo) icPanik(ad string) int {
v, tamam := d.ogeler[ad]
if !tamam {
panic(fmt.Sprintf("bulunamadi: %s", ad))
}
return v
}
func (d Depo) ortaPanik(ad string) int { return d.icPanik(ad) }
func (d Depo) disPanik(ad string) int { return d.ortaPanik(ad) }
func panikliyorMu(f func()) (p bool) {
defer func() {
if recover() != nil {
p = true
}
}()
f()
return false
}
func main() {
depo := Depo{ogeler: map[string]int{"a": 1}}
sonuc, hata := depo.disHata("yok")
gozle("hata donen imzanin gorunur kanali", "bildirim", "(int, error) -- ikinci deger hatayi tasiyor")
gozle("uc katmandan gecen hata donen cagri", "deger", fmt.Sprint(sonuc, " ", hata))
panikledi := panikliyorMu(func() { depo.disPanik("yok") })
gozle("panikleyen imzanin gorunur kanali", "hicbir", "int -- ikinci bir kanal yok")
gozle("uc katmandan gecen panikleyen cagri", "hicbir", fmt.Sprint("panik=", panikledi))
ozet()
}
hata donen imzanin gorunur kanali bildirim (int, error) -- ikinci deger hatayi tasiyor uc katmandan gecen hata donen cagri deger 0 bulunamadi: yok panikleyen imzanin gorunur kanali hicbir int -- ikinci bir kanal yok uc katmandan gecen panikleyen cagri hicbir panik=true bag: 1 bildirim, 1 deger, 2 hicbir yerde
HT18. icHatanın imzası (int, error) — başarısızlık kanalı doğrudan bildirimde
duruyor, orta ve dis bu ikinci değeri değiştirmeden taşıyor. icPanikin imzası ise yalnız
int; başarısızlık için ikinci bir yuva yok. panikliyorMu aracı (Fonksiyonlar dersinden
ödünç alınan derlenirMi gibi, K01’in araç setinden) burada da yalnız geçip geçmediğini
değil, panikleyip paniklemediğini gözlemliyor ve yığıt izi hiç basmıyor. Üçüncü katmandan
geçen çağrı gerçekten paniklemiş; ama bunu görebilmek, panikliyorMu gibi bir sarmalayıcı
yazmayı gerektirdi — sıradan bir çağrı (depo.disPanik("yok")) hiçbir işaret vermeden çöker.
HT19. Bu iki satır dersin merkezi gözlemini kuruyor: hata dönen biçimde başarısızlık
kanalı imzada bir yer kaplıyor (bildirim), panikleyen biçimde ise hiçbir yerde
kaplamıyor. Bu, konunun bildirim / kullanım yeri / değer üçlüsünün dışına çıkan dördüncü bir
durum — bağ ne kaynakta yazılı, ne derleyicinin bir atama noktasında denetlediği bir şey, ne
de değerin taşıdığı bir çift. icPaniki çağıran biri, imzaya bakarak bu işlevin başarısız
olabileceğini öğrenemiyor; bunu öğrenmenin tek yolu ya belgeleme (kaynağın dışında bir metin)
ya da işlevin gövdesini okumak.
Bu asimetrinin bir bedeli var, ama bedel iki yönde de işliyor. Hata dönen biçimde her katman
(ic, orta, dis) ikinci değeri elle taşımak zorunda — üç satırda üç kez return ..., err yazmak, ya da en azından üç kez ikinci değeri fark etmek gerekiyor. Panikleyen biçimde
katmanlar hiçbir şey yazmıyor; sıradan bir fonksiyon çağrısı yeterli, çünkü panik kendiliğinden
yayılıyor. Görünürlüğün bedeli, her katmanda tekrar eden bir yazım; görünmezliğin bedeli de,
çağıranın hiçbir uyarı almadan kalması.
Kurtarma Yalnız Ertelenmiş Çağrıda Çalışır
recover, bir paniği durdurup normal yürütmeye dönmenin tek yolu — ama yalnız tek bir konumda
çalışıyor: defer ile ertelenmiş bir işlevin doğrudan içinde. recover çağrısı bir
yardımcı işleve taşındığında, o yardımcı ertelenmiş işlev tarafından çağrılıyor olsa bile,
recover artık hiçbir şey yakalamıyor.
Bu kısıtlama rastgele değil: recoverin kendisi çalışma zamanına, çağrı yığıtının hangi
çerçevesinden çağrıldığını soruyor ve yalnız o çerçeve paniklemekte olan işlevin doğrudan
ertelediği çerçeve ise bir şey döndürüyor. Bu, recoveri sıradan bir yardımcı işlevin
içine gömüp yeniden kullanılabilir kılma — “bir kere yaz, her yerde kullan” biçimindeki
kısayolların — neden çoğu zaman işe yaramadığını açıklıyor.
// kurtar.go — recover yalniz kendisini dogrudan cagiran defer icinde calisiyor
package main
import "fmt"
func panikliyorMu(f func()) (p bool) {
defer func() {
if recover() != nil {
p = true
}
}()
f()
return false
}
func dogrudanKurtar() (kurtarildiMi bool) {
defer func() {
if recover() != nil {
kurtarildiMi = true
}
}()
panic("test")
}
func dolayliKurtarYardimcisi() interface{} {
return recover()
}
func dolayliKurtar() (kurtarildiMi bool) {
defer func() {
r := dolayliKurtarYardimcisi()
if r != nil {
kurtarildiMi = true
}
}()
panic("test")
}
func main() {
a := dogrudanKurtar()
gozle("recover dogrudan defer icinde cagrildi", "deger", fmt.Sprint(a))
disaKactiMi := panikliyorMu(func() { dolayliKurtar() })
gozle("recover bir yardimci uzerinden cagrildi, disariya kacti mi", "deger", fmt.Sprint(disaKactiMi))
ozet()
}
recover dogrudan defer icinde cagrildi deger true recover bir yardimci uzerinden cagrildi, disariya kacti mi deger true bag: 0 bildirim, 2 deger, 0 hicbir yerde
HT20. dogrudanKurtar, recover()i doğrudan defer func() { ... }() gövdesinde çağırıyor
ve paniği yakalıyor — kurtarildiMi true dönüyor. dolayliKurtar ise aynı şeyi yapmaya
çalışıyor gibi görünüyor: bir defer yazmış, o defer de bir işlev çağırıyor, o işlev de
recover()i çağırıyor. Ama arada bir çağrı katmanı olduğu için recover, kendisini çağıran
işlevin ertelenmiş olmadığını görüyor ve nil dönüyor — panik yakalanmıyor, dolayliKurtar
kendi dışına kaçıyor. Bu kaçışı görebilmek için onu bir panikliyorMu sarmalayıcısıyla
çağırmak gerekti; ikinci satır bu dış sarmalayıcının paniği yakaladığını gösteriyor.
HT21. Bu, recoverin dilde en dar tanımlı yerlerden biri olduğunu gösteriyor: yalnızca
“şu an çalışan işlev, panikleyen çağrının doğrudan ertelediği işlev mi” sorusuna bakıyor.
Çağrı derinliği önemli değil — dogrudanKurtarin kendisi panikliyorMunun içinde de
çağrılabilirdi, sonuç değişmezdi; önemli olan recoveri çağıran işlevin kendisinin
defer ile ertelenmiş olması. Bu kural, recoveri bir yardımcı işleve taşıyarak yeniden
kullanılabilir kılma girişimlerinin neden başarısız olduğunu açıklıyor.
Sınırlayıcı Ölçüm: Kurtarma Paniği Yok Etmez, Taşır
Bir panik kurtarıldığında, taşıdığı bilgi (paniğe verilen değer) kaybolmuyor — recover()
onu döndürüyor. Ama bu bilgiyi kullanmak çağıranın sorumluluğu. Aşağıdaki iki işlev aynı
paniği yakalıyor; biri onu bir errore çeviriyor, öbürü sessizce atıyor.
Bu iki işlev, panik ile hata arasındaki köprüyü de gösteriyor: bir panik kurtarıldıktan sonra,
sonucu tekrar Go’nun sıradan hata sözleşmesine — (sonuc, error) biçimine — sokmak tümüyle
mümkün. calistirKurtararak, bir kütüphanenin dış yüzeyinde panikleyen bir alt sistemi
saklayıp çağıranına sıradan bir error sunmak için sık kullanılan bir kalıbı örnekliyor.
// kurtarma.go — kurtarma panigi yok etmiyor, tasiyor; hataya cevrilmezse bilgi kayboluyor
package main
import (
"fmt"
"strings"
)
func unluSayisi(kelime string) int {
if kelime == "" {
panic("bos kelime")
}
sayi := 0
for _, r := range kelime {
if strings.ContainsRune("aeiouAEIOU", r) {
sayi++
}
}
return sayi
}
func calistirKurtararak(f func() int) (sonuc int, hata error) {
defer func() {
if r := recover(); r != nil {
hata = fmt.Errorf("kurtarilan panik (%T): %v", r, r)
}
}()
return f(), nil
}
func calistirSessizce(f func() int) (sonuc int) {
defer func() {
recover()
}()
return f()
}
func main() {
sonuc1, hata1 := calistirKurtararak(func() int { return unluSayisi("") })
gozle("bos kelimenin kurtarilan panigi hataya cevrildi", "deger", fmt.Sprint(sonuc1, " ", hata1))
sonucBos := calistirSessizce(func() int { return unluSayisi("") })
sonucGercekSifir := calistirSessizce(func() int { return unluSayisi("krt") })
gozle("bos kelimenin kurtarilan sonucu", "hicbir", fmt.Sprint(sonucBos))
gozle("unlusuz gercek kelimenin sonucu", "hicbir", fmt.Sprint(sonucGercekSifir))
ozet()
}
bos kelimenin kurtarilan panigi hataya cevrildi deger 0 kurtarilan panik (string): bos kelime bos kelimenin kurtarilan sonucu hicbir 0 unlusuz gercek kelimenin sonucu hicbir 0 bag: 0 bildirim, 1 deger, 2 hicbir yerde
HT22. calistirKurtararak, kurtardığı değeri %T ile tipiyle birlikte bir errore
çeviriyor — unluSayisi("") paniklediğinde verdiği değer bir dizgi (string), ve bu dönen
hatada görünüyor. Kurtarma, paniği yok etmiyor; taşıdığı bilgiyi bir başka biçimde, imzanın
zaten sahip olduğu bir kanala (error) aktarıyor. Bu satır, bir panik kurtarıldığında bile
%Tnin hâlâ hangi değerin panikletildiğini gösterebildiğini kanıtlıyor.
HT23. calistirSessizce ise aynı paniği yakalıyor ama recover()in döndürdüğü değeri
hiçbir yere yazmıyor. Sonuç: unluSayisi("") panikleyip kurtarıldığında dönen 0 ile
unluSayisi("krt")nin (hiç ünlü içermeyen, gerçekten hesaplanmış bir dizgi) döndürdüğü 0
birbirinden ayırt edilemiyor. Bu, dersin sınırlayıcı ölçümü: kurtarma bilgiyi taşıyor, ama
kurtarılan değer bir yere yazılmazsa, o taşınan bilgi çağıranın erişemeyeceği bir yerde
tümüyle kayboluyor — geriye “başarılı gibi görünen” bir sıfır kalıyor. Bu örnekte sıfırın
kendisi bile bir tuzak: unluSayisi("krt") gerçekten hesaplanmış, meşru bir sıfırdır; panik
kaynaklı sıfırla aralarında hiçbir işaret farkı yok. Genel olarak, kurtarma sonrası sessizce
bırakılan bir dönüş değeri, tipinin sıfır değeriyle her zaman karışabilir — bu, hangi tip
seçilirse seçilsin geçerli bir risk.
Bu üç ölçüm birlikte konunun sorusuna kesin bir yanıt veriyor: paniğin bağı bildirimde değil (imzada hiçbir iz yok), kullanım yerinde değil (derleyicinin baktığı hiçbir atama noktası yok) ve değerde de değil (çağıran, panikleme ihtimalini herhangi bir tipten okuyamıyor) — panik, konunun üç yerinin hiçbirinde kurulmuyor. Tek kurulma noktası, kurtarma koduna kadar hiç görünmeyen çalışma zamanı; ve kurtarma bile, bilgiyi başka bir kanala elle taşımadıkça hiçbir şeyi telafi etmiyor.
Özet
- Hata dönen bir işlevin imzası başarısızlık için bir kanal ayırır (
bildirim); panikleyen bir işlevin imzasında bu kanal hiç yoktur — panik konunun üç yerinin hiçbirinde kurulmaz. recover, yalnızca kendisini doğrudan çağıran ertelenmiş (defer) işlevin içinde çalışır; bir yardımcı işleve taşındığındanildöner ve panik yakalanmadan yayılmaya devam eder.- Bir panik kurtarıldığında taşıdığı değer kaybolmaz;
%Tile tipiyle birlikte okunabilir ve birerrore çevrilebilir. - Kurtarılan değer bir yere yazılmazsa bilgi tümüyle kaybolur: gerçek bir sıfır sonuçla kurtarılmış bir paniğin sıfır sonucu ayırt edilemez hâle gelir.
- Yığıt izi, bellek adresi ve goroutine numarası hiçbir ölçümde basılmaz; yalnız
panik=true/panik=falseve kurtarılan değerin%Tile okunan tipi kaydedilir.
Sonraki Adım
Bu konu tek bir soruyu dört kez sordu: error arayüzünü bir tip nasıl karşılıyor, bir hatanın
kimliği nerede duruyor, bir zincirin bağlamı nasıl korunuyor, ve son olarak bir çıkışın bağı
hiç kurulmadığında ne oluyor. İlk üç ders, hep aynı yanıtı buldu: bağ kullanım yerinde
kuruluyor (bir değişkene atama, bir yöntem kümesi denetimi) ya da değerin içinde taşınıyor
(dinamik tip, kimlik, sarmalanmış zincir). Bu ders, bu ikisinin de dışında kalan dördüncü bir
durumu ölçtü: paniğin bağı hiçbir yerde kurulmuyor ve imzada hiç görünmüyor — çağıran, bir
işlevin panikleyebileceğini imzasından öğrenemiyor.
Sıradaki konu, genellikler, bunun tam tersini yapıyor. Bir genel fonksiyonun tip parametresi de kaynakta hiç yazılmıyor — çağıran hiçbir tip belirtmeden bir işlevi çağırabiliyor. Ama panikten farklı olarak bu bağı derleyici kuruyor, ve tam olarak çağrı yerinde: her çağrı, hangi tiple örneklendiğini derleme zamanında belirliyor. Aynı “kaynakta yazılmama” gözlemi, iki konuda birbirinin tam tersi iki sonuca varıyor — panik her denetimi kaybediyor, genellik her denetimi kullanım yerine taşıyor ve bağ hiçbir zaman bir değerin içine girmiyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.