go 的 revover 实现原理
recover() 函数实际被调用的是src/runtime/panic.go:gorecover()
gorecover
runtime.gorecover 函数实现很简短
func gorecover(arg uintptr) interface{} {
gp := getg()
p := gp._panic() // 获取panic实例,只有发生了panic,实例才不为nil
if p != nil && !p.goexit && !p.recovered && arg == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
-
恢复逻辑
runtime.gorecover()
函数通过协程数据结构中的_panic
得到当前 panic 实例(上面代码中的 p),如果当前panic的状态支持recover,则给该 panic实例标记 recovered状态(p.recovered= true
),最后返回panic()
函数的参数(p.arg)。另外,当前执行
recover()
函数的 defer 函数是被runtime.gopanic()
执行的,defer 函数执行 结束以后,在runtime.gopanic()
函数中会检查panic实例的recovered状态,如果发现 panic被恢 复,则runtime.gopanic()
将结束当前panic流程,将程序流程恢复正常。
-
生效条件
通过代码p != nil && !p.goexit && !p.recovered && arg == uintptr(p.argp)
可以看到需要满足四个条件才可以恢复panic,且四个条件缺一不可:- p!=nil:必须存在
panic
- !p.goexit:非
runtime.goexit
- !p.recovered:
panic
还未被恢复; - argp == uintptr(p.argp):
recover
必须被defer()
直接调用。
当前协程没有产生panic时,协程结构体中panic的链表为空,不满足恢复条件。
当程序运行
runtime.goexit
时也会创建一个 panic 实例,会标记该实例的goexit
属性为 true,但该类型的 panic 不能被恢复。假设函数包含多个defer函数,前面的 defer通过 recover() 函数消除panic后,函数中剩余的 defer 仍然会执行,但不能再次
recover()
,如以下代码所示,函数第一行 defer 中的 recover() 将返回 nil。func foo() { defer func() { recover() }() // 恢复无效,因为_panic.recovered ==true defer func() { recover() }() // 标记_panic.recovered=true panic("err") }
内置函数
recover()
没有参数,runtime.gorecover()
函数却有参数,为什么呢?
这正是为了限制recover()
函数必须被defer直接调用。runtime.gorecover()
函数的参数为调用recover()
函数的参数地址,通常是 defer 函数的参数地址,panic 实例中也保存了当前 defer函数的参数地址,如果二者一致,说明recover
被defer函数直接调用。举例如下:func foo() { defer func() { // 假设函数为A func() { // 假设函数为B // runtime.gorecover,传入函数B的参数地址 // argp==uintptr(p.argp),检测失败,无法恢复 if err :=recover(); err!=nil { fmt.Print1n("A") } } } }
- p!=nil:必须存在
总结
通过以上分析,我们可以很好地回答以下问题了:
-
为什么
recover()
函数一定要在defer()
函数中才生效?
如果recover()
函数不在defer()
函数中,那么defer()
函数可能出现在panic之前,也可能出现在panic之后。出现在panic之前,因为找不到panic实例而无法生效,出现在panic之后,代码没有机会执行,所以recover()
函数必须存在于recover()
函数中才会生效。 -
panic 被 recover 之后,无法再次被 recover 捕获
-
假如
defer()
函数中调用了函数A,为什么A中的recover()
不能生效?