Skip to content

panic

panic هي دالة مدمجة في Go، عندما يُواجه خطأ غير قابل للاسترداد، غالبًا ما يُلقي البرنامج panic، مثل الوصول الشائع لمؤشر nil:

go
func main() {
  var a *int
  *a = 1
}

عند تشغيل الكود أعلاه، سيُلقي البرنامج panic التالي، ثم يتوقف:

panic: runtime error: invalid memory address or nil pointer dereference

في بعض الحالات، نستدعي يدويًا الدالة panic لجعل البرنامج يخرج، لتجنب عواقب أسوأ. عادةً أيضًا نستخدم دالة مدمجة أخرى recover لالتقاط panic، وتُستخدم مع defer:

go
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  var a *int
  *a = 1
}

لماذا يجب استخدام الدالة recover داخل defer، وما الذي تفعله recover؟ هذه الأسئلة ستجيب عليها المحتويات التالية.

البنية

لـ panic في وقت التشغيل بنية مقابلة، وهي runtime._panic، وبنيتها ليست معقدة:

go
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 هو معامل دالة panic
  • argp يشير لمعاملات defer، وعند حدوث panic يُفعَّل تنفيذ defer
  • aborted يشير إلى ما إذا كان قد أُوقف قسرًا

panic مثل defer، يوجد كقائمة مرتبطة في الكوروتين:

go
type g struct {
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
}

الذعر

سواءً استدعينا panic يدويًا، أو حدث panic في البرنامج، في النهاية سيدخل في الدالة runtime.gopanic:

go
func gopanic(e any)

في البداية، أولًا يُتحقق مما إذا كان المعامل nil، إذا كان كذلك سيُنشأ خطأ من النوع runtime.PanicNilError:

go
if e == nil {
    if debug.panicnil.Load() != 1 {
        e = new(PanicNilError)
    } else {
        panicnil.IncNonDefault()
    }
}

ثم يُضاف panic الحالي لرأس القائمة المرتبطة للكوروتين:

go
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

بعد ذلك يدخل حلقة for لمعالجة القائمة المرتبطة defer للكوروتين الحالي واحدًا تلو الآخر:

go
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:

go
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 أي غير قابلة للاسترداد:

go
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:

go
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:

go
for {
    ...
      d.fn()
      ...
    if p.recovered {
      ...
    }
}

منطق الاسترداد بعد تنفيذ defer، وهنا يتبين لماذا لا يمكن استخدام الدالة recover إلا داخل defer، فإذا استُخدمت recover خارج defer فإن gp._panic ستساوي nil، وبطبيعة الحال لن يُضبَط p.recovered على true، وبالتالي في الدالة gopanic لن يدخل منطق الاسترداد:

go
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 للتدفق المنطقي الطبيعي للدالة المستخدم:

go
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 يمكن رؤية المحتوى التالي:

go
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 تعود الدالة مباشرة.

Golang تم تحريره بواسطة www.golangdev.cn