Skip to content

panic

panic เป็นฟังก์ชัน built-in ของ go เมื่อพบข้อผิดพลาดที่ไม่สามารถกู้คืนได้ โปรแกรมมักจะโยน panic เช่น การเข้าถึงตัวชี้ว่างที่พบได้บ่อย

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

เมื่อรันโค้ดด้านบน โปรแกรมจะโยน panic ดังต่อไปนี้ แล้วโปรแกรมจะหยุด

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

ในบางกรณี เราจะเรียกฟังก์ชัน panic ด้วยตนเองเพื่อให้โปรแกรมออก เพื่อหลีกเลี่ยงผลลัพธ์ที่รุนแรงกว่า นอกจากนี้ยังใช้ฟังก์ชัน built-in อีกตัว 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 ชี้ไปยัง execution context ของฟังก์ชันที่เรียกเพื่อความสะดวกในการกู้คืนในภายหลัง
  • 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 หรือไม่ หากเป็น nil จะ new ข้อผิดพลาดประเภท 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 ข้อมูล call stack ที่เรามักเห็นถูกส่งออกโดยมัน สุดท้ายฟังก์ชัน runtime.exit จะออกโปรแกรมผ่านการเรียก系統 _ExitProcess

การกู้คืน

ผ่านการเรียกฟังก์ชัน built-in 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 by www.golangdev.cn edit