前言
本文是探讨的是"recover函数为什么一定要在defer里面才生效
"
此文章是个人学习归纳的心得, 如有不对, 还望指正, 感谢!
热身
请分析下面代码的运行结果
go
package main
import "fmt"
func main(){
defer func(){
func(){
if err := recover(); err != nil {
fmt.Println("A")
}
}()
}()
panic("demo") //触发惊恐
fmt.Println("B")
}
运行结果:
从运行结果,我们可以得知,recover并没有捕获到惊恐,而是由惊恐引发了程序崩溃
乍一看,可能这个例子与我们探索的目标可能没什么关系,但是这个例子考验了你对recover的认识,且听我徐徐道来
panic是什么?
可以类比其他语言中的异常,panic出现的时候,Go程序即将崩溃,至于为什么是"即将",那是因为我们还可以通过recover函数来进行捕获,来挽救Go程序,使其正常运行,在Go语言中,忽略panic是一种有意识的行为。
recover
函数就是为了捕获panic
然后阻止程序崩溃的,要想了解recover
,我们得先来认识panic
,也就是我所谓的惊恐
panic的结构
panic的底层源码如下
rust
type _panic struct {
argp unsafe.Pointer // 指向在 panic 期间运行的延迟调用的参数的指针;不能移动 - 已知由 liblink 处理
arg any // panic 的参数,存储panic()函数传入值的
link *_panic // 指向先前 panic 的链接
pc uintptr // 如果绕过此 panic,返回到 runtime 中的位置
sp unsafe.Pointer // 和上面pc效果一样,但使用方法不一样
recovered bool //标志这个panic是否已经被recover()恢复
aborted bool // 标志当前_panic是否被中止
goexit bool // 标志当前实例是否由runtime.Goexit()产生的
}
具体定义在 src/runtime/runtime2.go
其中我们需要关注的是recovered
属性,recover
函数主要是通过修改这个属性来标志是否处理panic
。
值得一提的是,goexit
属性,是用来标识当前goroutine
是否为已退出的,Goexit
函数产生的_panic
会被标识,然后这个_panic
就不会被recove
函数捕获了。
当我们使用 panic("这是一个惊恐!")
的时候,就会产生一个_panic
实例,值会存到 arg
属性里面
recover函数是什么?
中文含义为"恢复",是一个内置函数,用于捕获程序中的异常,使程序回到正常流程
recover()的源码
在src/builtin/builtin.go
中我们可以找到它
go
func recover() any
可惜的是,这并不是我们想要的,我们需要通过分析它在运行时的代码结构
使用工具找运行时的代码
我们可以使用go编译器自带的工具来从汇编进行分析
新建一个demo.go的文件,键入如下代码
go
package main
func main() {
defer func() {
recover()
}()
}
然后是 go tool compile -S 文件路径
,进行汇编展示。
我的是这个
然后通过对应的行数找到对应的运算,如下图
通过这个,我们找到了运行时的recover()
的真实面貌
也就是 runtime.gorecover()
函数
真实源码
在src/runtime/panic.go
中我们可以找到它,那我们也离揭开recovr()函数能捕获panic和为什么一定要在defer里面执行的谜题不远了
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
}
看到这个函数时,我的第一反应是,为什么recover()
没有传参,怎么gorecover
函数要传参?
其实这个参数是编译运行的时候,解释器自动塞入的,塞的是指向调用recover()的父函数
先不要急,我们先看函数里面的结构,分析一下具体执行流程
-
首先通过 内置函数
getg()
得到指向当前协程的指针。 -
然后取出当前协程的_panic,也就是惊恐,如果没有惊恐,那就是nil
-
接下来通过判断一系列条件之后,
决定是否将_panic的recovered属性改为true并返回arg
前面我在介绍惊恐的原型------_panic
的时候提到过recovered
属性和arg
属性,recovered
是用来标识是否被recover
处理过的,arg
是用来存储panic()
函数的传入参数的。
而关键点就在这个判断条件上:
-
第一个条件是
p != nil
也就是我们从这个协程中取出的_panic
不为nil
,这个协程确确实实出现了panic惊恐
,因为recover
就是用来处理panic
用的 -
第二个判断条件就是
!p.goexit
,意思是这个panic
不是由runtime.Goexit()
产生的,Goexit
函数在运行时会产生一个他自己使用的panic
,为了避免被误处理,所以加了这个属性。 -
第三个判断条件是
!p.recovered
,意思是当前panic
没有被recoverv
处理过,因为重复处理,没有意义了,所以在defer
中多次调用recover
,也只有第一次的会生效 -
最后一个是
argp == uintptr(p.argp)
,argp
是编译运行的时候,解释器自动塞入的,塞的是指向调用recover()
的父函数,而argp
属性,我们也在前面讲_panic
时也提到过,它是_panic
的第一个属性,这个属性存放的是指向在panic
期间运行的延迟调用的参数的指针,也就是当前recover
函数所在的defer
函数,当argp
和uintptr(p.argp)
不相等的时候,也就是说明,当前recover
函数不在defer
里面,然后就没有进入if的内部语句,直接return nil
了
那这个判断recover
在不在defer
里面的意义在哪?
其实是这样的,在一个普通的协程中,recover
不在defer
中的话,那就是按顺序执行了,如果当时并没有panic的话,那recover就没有任何作用,毕竟这个函数的设计就是为了把快要崩溃的程序进行挽救,所以我们只有把这个函数放到defer中执行,它挽救快要崩溃的程序的功能才能发挥。
总结
recover运行的条件:
- 该协程必须出现了panic
- recover函数必须在和panic同级的defer中被调用