panic
panic هي دالة مدمجة في Go، عندما يُواجه خطأ غير قابل للاسترداد، غالبًا ما يُلقي البرنامج panic، مثل الوصول الشائع لمؤشر nil:
func main() {
var a *int
*a = 1
}عند تشغيل الكود أعلاه، سيُلقي البرنامج panic التالي، ثم يتوقف:
panic: runtime error: invalid memory address or nil pointer dereferenceفي بعض الحالات، نستدعي يدويًا الدالة panic لجعل البرنامج يخرج، لتجنب عواقب أسوأ. عادةً أيضًا نستخدم دالة مدمجة أخرى recover لالتقاط panic، وتُستخدم مع defer:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}لماذا يجب استخدام الدالة recover داخل defer، وما الذي تفعله recover؟ هذه الأسئلة ستجيب عليها المحتويات التالية.
البنية
لـ panic في وقت التشغيل بنية مقابلة، وهي runtime._panic، وبنيتها ليست معقدة:
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
}بنيتها مشابهة جدًا لـ defer:
linkيشير لبنية_panicالتاليةpcوspيشيران لموقع التنفيذ في الدالة المستدعية لتسهيل الاسترداد لاحقًاargهو معامل دالةpanicargpيشير لمعاملاتdefer، وعند حدوثpanicيُفعَّل تنفيذdeferabortedيشير إلى ما إذا كان قد أُوقف قسرًا
panic مثل defer، يوجد كقائمة مرتبطة في الكوروتين:
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
الذعر
سواءً استدعينا panic يدويًا، أو حدث panic في البرنامج، في النهاية سيدخل في الدالة runtime.gopanic:
func gopanic(e any)في البداية، أولًا يُتحقق مما إذا كان المعامل nil، إذا كان كذلك سيُنشأ خطأ من النوع runtime.PanicNilError:
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}ثم يُضاف panic الحالي لرأس القائمة المرتبطة للكوروتين:
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))بعد ذلك يدخل حلقة for لمعالجة القائمة المرتبطة defer للكوروتين الحالي واحدًا تلو الآخر:
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)))
...
}إذا كان defer الحالي قد فُعِّل بالفعل بواسطة panic آخر، أي _defer.started == true، فإن panic الأقدم لن يُنفَّذ. ثم تُنفَّذ الدالة المقابلة لـ defer:
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)بعد الانتهاء من التنفيذ تُسترد بنية defer الحالية، ويستمر تنفيذ defer التالي، وعند الانتهاء من تنفيذ جميع هياكل defer دون استرداد خلالها، سيدخل الدالة runtime.fatalpanic، وهذه الدالة unrecoverable أي غير قابلة للاسترداد:
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
}خلال هذه الفترة ستُطبِع printpanics معلومات panic، ومعلومات مكدس الاستدعاء التي نراها عادةً هي من مخرجاتها، وأخيرًا الدالة runtime.exit تخرج من البرنامج عبر الاستدعاء النظامي _ExitProcess.
الاسترداد
عند استدعاء الدالة المدمجة recover، أثناء الترجمة ستتحول لاستدعاء الدالة runtime.gorecover:
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
}تنفيذها بسيط جدًا، كل ما تفعله هو p.recovered = true، والكود المسؤول فعليًا عن معالجة منطق الاسترداد موجود في الدالة gopanic:
for {
...
d.fn()
...
if p.recovered {
...
}
}منطق الاسترداد بعد تنفيذ defer، وهنا يتبين لماذا لا يمكن استخدام الدالة recover إلا داخل defer، فإذا استُخدمت recover خارج defer فإن gp._panic ستساوي nil، وبطبيعة الحال لن يُضبَط p.recovered على true، وبالتالي في الدالة gopanic لن يدخل منطق الاسترداد:
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")
}عند الاسترداد تُنظَّف تلك panic الموقفة قسرًا من القائمة المرتبطة، ثم تُدخَل الدالة runtime.recovery، وتعود عبر runtime.gogo للتدفق المنطقي الطبيعي للدالة المستخدم:
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)
}ثم هناك نقطة مهمة يجب الانتباه لها، هذا السطر من الكود:
gp.sched.ret = 1يضبط قيمة ret على 1، ومن تعليق الدالة runtime.deferproc يمكن رؤية المحتوى التالي:
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()
}الكود الوسيط الذي يُنتجه المترجم يتحقق مما إذا كانت هذه القيمة 1، إذا كانت كذلك فسينفذ مباشرة الدالة runtime.deferreturn، وعادةً هذه الدالة لا تُنفَّذ إلا قبل إرجاع الدالة، وهذا يوضح لماذا بعد recover تعود الدالة مباشرة.
