panic
panic เป็นฟังก์ชัน built-in ของ go เมื่อพบข้อผิดพลาดที่ไม่สามารถกู้คืนได้ โปรแกรมมักจะโยน panic เช่น การเข้าถึงตัวชี้ว่างที่พบได้บ่อย
func main() {
var a *int
*a = 1
}เมื่อรันโค้ดด้านบน โปรแกรมจะโยน panic ดังต่อไปนี้ แล้วโปรแกรมจะหยุด
panic: runtime error: invalid memory address or nil pointer dereferenceในบางกรณี เราจะเรียกฟังก์ชัน panic ด้วยตนเองเพื่อให้โปรแกรมออก เพื่อหลีกเลี่ยงผลลัพธ์ที่รุนแรงกว่า นอกจากนี้ยังใช้ฟังก์ชัน built-in อีกตัว 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ชี้ไปยัง execution context ของฟังก์ชันที่เรียกเพื่อความสะดวกในการกู้คืนในภายหลัง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 หรือไม่ หากเป็น nil จะ new ข้อผิดพลาดประเภท 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 ข้อมูล call stack ที่เรามักเห็นถูกส่งออกโดยมัน สุดท้ายฟังก์ชัน runtime.exit จะออกโปรแกรมผ่านการเรียก系統 _ExitProcess
การกู้คืน
ผ่านการเรียกฟังก์ชัน built-in 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 แล้วฟังก์ชันจะกลับโดยตรง
