首页 > 百科 > 服务器 > 正文

recover的真身

就像我们之前针对panic做的一样,我们也写一段简单的代码,通过其汇编码尝试找出内置函数recover()的底层实现。

编写以下简单的代码,并保存在名为compile.go的文件里:

//recover/compile.go

packagerecover

funccompile(){
deferfunc(){
recover()
}()
}

然后使用以下命令编译代码:

gotoolcompile-Srecover/compile.go

接着根据代码行号找出recover()语句对应的汇编码:

0x002400036(recover/compile.go:5)PCDATA$0,$1
0x002400036(recover/compile.go:5)PCDATA$1,$0
0x002400036(recover/compile.go:5)LEAQ""..fp+40(SP),AX
0x002900041(recover/compile.go:5)PCDATA$0,$0
0x002900041(recover/compile.go:5)MOVQAX,(SP)
0x002d00045(recover/compile.go:5)CALLruntime.gorecover(SB)

我们可以看到recover()函数调用被替换成了runtime.gorecover()函数。runtime.gorecover()实现源码位于src/runtime/panic.go。

gorecover()

runtime.gorecover()函数实现很简短:

funcgorecover(argpuintptr)interface{}{
gp:=getg()
//获取panic实例,只有发生了panic,实例才不为nil
p:=gp._panic
//recover限制条件
ifp!=nil&&!p.goexit&&!p.recovered&&argp==uintptr(p.argp){
p.recovered=true
returnp.arg
}
returnnil
}

短短的代码,蕴含的信息量却很大。 它可以解释以下问题:

  • recover()到底是如何恢复panic的?

  • 为什么recover()一定要在defer()函数中才生效?

  • 假如defer()函数中调用了函数A(),为什么A()中的recover()不能生效?

恢复逻辑

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流程,将程序流程恢复正常。

生效条件

通过代码的if语句可以看到需要满足四个条件才可以恢复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。

funcfoo(){
deferfunc(){recover()}()//恢复无效,因为_panic.recovered=true
deferfunc(){recover()}()//标记_panic.recovered=true
panic("err")
}

细心的读者或许会发现,内置函数recover()没有参数,runtime.gorecover()函数却有参数,为什么呢? 这正是为了限制recover()必须被defer()直接调用。

runtime.gorecover()函数的参数为调用recover()函数的参数地址,通常是defer函数的参数地址,同地_panic实例中也保存了当前defer函数的参数地址,如果二者一致,说明recover()被defer函数直接调用。举例如下:

funcfoo(){
deferfunc(){//假设函数为A
func(){//假设函数为B
//runtime.gorecover(B),传入函数B的参数地址
//argp==uintptr(p.argp)检测失败,无法恢复
iferr:=recover();err!=nil{
fmt.Println("A")
}
}()
}()
}
设计思路

通过以上源码的分析,我们可以很好地回答以下问题了:

  • 为什么recover()一定要在defer()函数中才生效?

  • 假如defer()函数中调用了函数A(),为什么A()中的recover()不能生效?

如果recover()不在defer()函数中,那么recover()可能出现在panic()之前,也可能出现在panic()之后,出现在panic()之前,因为找不到panic实例而无法生效,出现在panic()之后,代码没有机会执行,所以recover()必须存在于defer函数中才会生效。

通过上面的分析,从代码层面我们理解了为什么recover()函数必须被defer直接调用才会生效。但为什么要有这样的设计呢?

笔者也没有找到官方关于此设计的资料,不过笔者认为此设计非常合理。

考虑下面的代码:

funcfoo(){
deferfunc(){
thirdPartPkg.Clean()//调用第三方包清理资源
}()

iferr!=nil{//条件不满足触发panic
panic(xxx)
}
}

有时我们会在代码里显式地触发panic,同时往往还会在defer函数里调用第三方包清理资源,如果第三方包也使用了recover(),那么我们触发的panic将会被拦截,而且这种拦截可能是非预期的,并不我们期望的结果。

猜你喜欢
文章评论已关闭!
picture loss