panic 发生之后,如果 Go 不做任何特殊处理,默认行为是打印堆栈,退出程序。
panic 到底是什么?
什么叫做 panic( ) 的对应的处理?
循环执行 goroutine 上面的 _defer 函数链,如果执行完了都还没有恢复 _panic 的状态,那就没得办法了,退出进程,打印堆栈。
如果在 goroutine 的 _defer 链上,有个朋友 recover 了一下,把这个 _panic 标记成恢复,那事情就到此为止,就从这个 _defer 函数执行后续正常代码即可,走 deferreturn 的逻辑。
recover 函数
recover 对应了 runtime/panic.go 中的 gorecover 函数实现。
func gorecover(argp uintptr) interface{} {
// 只处理 gp._panic 链表最新的这个 _panic;
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
这个函数可太简单了:
这就是 recover 函数的全部内容,只给 _panic.recovered 赋值而已,不涉及代码的神奇跳转。而 _panic.recovered 的赋值是在 panic 函数逻辑中发挥作用。
panic函数
panic 的实现在一个叫做 gopanic 的函数,位于 runtime/panic.go 文件。panic 机制最重要最重要的就是 gopanic 函数了,所有的 panic 细节尽在此。为什么 panic 会显得晦涩,主要有两个点:
一切秘密都在下面这个函数:
// runtime/panic.go
func gopanic(e interface{}) {
// 在栈上分配一个 _panic 结构体
var p _panic
// 把当前最新的 _panic 挂到链表最前面
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
// 取出当前最近的 defer 函数;
d := gp._defer
if d == nil {
// 如果没有 defer ,那就没有 recover 的时机,只能跳到循环外,退出进程了;
break
}
// 进到这个逻辑,那说明了之前是有 panic 了,现在又有 panic 发生,这里一定处于递归之中;
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
// 把这个 defer 从链表中摘掉;
gp._defer = d.link
freedefer(d)
continue
}
// 标记 _defer 为 started = true (panic 递归的时候有用)
d.started = true
// 记录当前 _defer 对应的 panic
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 执行 defer 函数
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
// defer 执行完成,把这个 defer 从链表里摘掉;
gp._defer = d.link
// 取出 pc,sp 寄存器的值;
pc := d.pc
sp := unsafe.Pointer(d.sp)
// 如果 _panic 被设置成恢复,那么到此为止;
if p.recovered {
// 摘掉当前的 _panic
gp._panic = p.link
// 如果前面还有 panic,并且是标记了 aborted 的,那么也摘掉;
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
// panic 的流程到此为止,恢复到业务函数堆栈上执行代码;
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 注意:恢复的时候 panic 函数将从此处跳出,本 gopanic 调用结束,后面的代码永远都不会执行。
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 打印错误信息和堆栈,并且退出进程;
preprintpanics(gp._panic)
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
上面逻辑可以拆分为循环内和循环外两部分去理解:
for 循环内
循环内的事情拆解成:
问题一:为什么 recover 一定要放在 defer 里面才生效?
因为,这是唯一的修改 _panic.recovered 字段的时机 !
为什么 recover 已经放在 defer 里面,但是进程还是没有恢复?
划重点:在 gopanic 里,只遍历执行当前 goroutine 上的 _defer 函数链条。所以,如果挂在其他 goroutine 的 defer 函数做了 recover ,那么没有丝毫用途。
例
func main() { // g1
go func() { // g2
defer func() {
recover()
}()
}()
panic("test")
}
因为,panic 和 recover 在两个不同的 goroutine,_panic 是挂在 g1 上的,recover 是在 g2 的 _defer 链条里。
gopanic 遍历的是 g1 的 _defer 函数链表,跟 g2 八杆子打不着,g2 的 recover 自然拿不到 g1 的 _panic 结构,自然也不能设置 recovered 为 true ,所以程序还是崩了。
recover 函数
在 gopanic 函数中,在循环执行 defer 函数的时候,如果发现 _panic.recovered 字段被设置成 true 的时候,调用 mcall(recovery) 来执行所谓的恢复。
看一眼 recovery 函数的实现,这个函数极其简单,就是恢复 pc,sp 寄存器,重新把 Goroutine 投递到调度队列中。
// runtime/panic.go
func recovery(gp *g) {
// 取出栈寄存器和程序计数器的值
sp := gp.sigcode0
pc := gp.sigcode1
// 重置 goroutine 的 pc,sp 寄存器;
gp.sched.sp = sp
gp.sched.pc = pc
// 重新投入调度队列
gogo(&gp.sched)
}
总结
参考