panic
panic Go'nun yerleşik fonksiyonudur. Kurtarılamaz hatalarla karşılaşıldığında, program genellikle bir panic fırlatır, örneğin yaygın nil işaretçi erişimi:
func main() {
var a *int
*a = 1
}Yukarıdaki kodu çalıştırmak programın aşağıdaki panic'i fırlatmasına, sonra programın durmasına neden olur.
panic: runtime error: invalid memory address or nil pointer dereferenceBazı durumlarda, daha ciddi sonuçlardan kaçınmak için programın çıkışını sağlamak amacıyla manuel olarak panic fonksiyonunu da çağırırız. Ayrıca panic'i yakalamak için başka bir yerleşik fonksiyon recover'ı kullanırız ve bunu defer ile birlikte kullanırız.
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}Neden recover fonksiyonu defer içinde kullanılmalıdır? recover ne iş yapar? Bu sorular aşağıdaki içerikte cevaplanacaktır.
Yapı
panic'in de runtime'da karşılık gelen bir yapısı vardır, bu runtime._panic'tir. Yapısı karmaşık değildir, aşağıda gösterildiği gibi.
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg any // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}Yapısı defer ile çok benzerdir:
linkbir sonraki_panicyapısına işaret ederpcvespdaha sonra kurtarma için çağırma fonksiyonunun yürütme bağlamına işaret ederargpanicfonksiyonunun parametresidirargpdefer'in parametrelerine işaret eder;panicoluştuğunda,deferyürütmesini tetiklerabortedzorla durdurulup durdurulmadığını gösterir
panic, defer gibi, bağlı liste olarak goroutine'lerde bulunur:
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
Panic
panic fonksiyonunu aktif olarak çağırsak da veya program bir panic yaşasa da, sonunda runtime.gopanic fonksiyonuna girer:
func gopanic(e any)Başlangıçta, önce parametrenin nil olup olmadığını kontrol eder. Eğer nil ise, bir runtime.PanicNilError tipi hata oluşturur:
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Sonra mevcut panic'i goroutine'un bağlı listesinin başına ekler:
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Sonra bir for döngüsüne girer ve mevcut goroutine'un defer bağlı listesini tek tek işlemeye başlar:
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
}Eğer mevcut defer zaten başka bir panic tarafından tetiklendiyse, yani _defer.started == true, o zaman daha önceki panic yürütülmez. Sonra defer'e karşılık gelen fonksiyonu yürütür:
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)Yürüttükten sonra, mevcut defer yapısını geri dönüştürür ve bir sonraki defer'i yürütmeye devam eder. Tüm defer'ler yürütüldüğünde ve süreçte hiçbiri kurtarılmadığında, runtime.fatalpanic fonksiyonuna girer, bu kurtarılamaz:
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
runningPanicDefers.Add(-1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}Bu süreçte, panic bilgisini yazdırmak için printpanics çağırılır. Genellikle gördüğümüz çağrı yığını bilgisi bunun tarafından çıktılanır. Son olarak, runtime.exit fonksiyonu programı sistem çağrısı _ExitProcess ile sonlandırır.
Kurtarma
Yerleşik fonksiyon recover'ı çağırarak, derleme sırasında runtime.gorecover fonksiyonuna bir çağrı olur:
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}Uygulaması çok basittir, sadece bir şey yapar: p.recovered = true. Kurtarma mantığını işlemekten sorumlu olan kod aslında gopanic fonksiyonundadır:
for {
...
d.fn()
...
if p.recovered {
...
}
}Kurtarma mantığı defer yürütmesinden sonradır. Bu noktada, neden recover fonksiyonunun sadece defer'de kullanılabileceğini anlıyoruz. Eğer recover defer dışında kullanılırsa, gp._panic nil olur ve doğal olarak p.recovered true olarak ayarlanmaz, bu yüzden gopanic fonksiyonu kurtarma mantığına girmez.
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}Kurtarma sırasında, bağlı listede zaten zorla durdurulan panic'leri temizler, sonra runtime.recovery fonksiyonuna girer. runtime.gogo aracılığıyla, kullanıcı fonksiyonunun normal mantık akışına döner:
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}Önemli bir dikkat edilmesi gereken nokta bu satırdır:
gp.sched.ret = 1ret değerini 1 olarak ayarlar. runtime.deferproc fonksiyon yorumlarından, aşağıdaki içeriği görebiliriz:
func deferproc(fn func()) {
...
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
}Derleyicinin ürettiği ara kod bu değerin 1 olup olmadığını kontrol eder. Eğer öyleyse, doğrudan runtime.deferreturn fonksiyonunu yürütür. Genellikle, bu fonksiyon sadece fonksiyon dönmeden önce yürütülür, bu da recover'dan sonra fonksiyonun neden doğrudan döndüğünü açıklar.
