İçeriğe geç
academia.sh

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ığında nil döner ve panik yakalanmadan yayılmaya devam eder.
  • Bir panik kurtarıldığında taşıdığı değer kaybolmaz; %T ile tipiyle birlikte okunabilir ve bir errore ç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=false ve kurtarılan değerin %T ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat