共计 3009 个字符,预计需要花费 8 分钟才能阅读完成。
本篇内容介绍了“Go 编程中 recover 源码是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
recover 的真身
就像我们之前针对 panic 做的一样,我们也写一段简单的代码,通过其汇编码尝试找出内置函数 recover() 的底层实现。
编写以下简单的代码,并保存在名为 compile.go 的文件里:
// recover/compile.go
package recover
func compile() {defer func() {recover()
}
然后使用以下命令编译代码:
go tool compile -S recover/compile.go
接着根据代码行号找出 recover() 语句对应的汇编码:
0x0024 00036 (recover/compile.go:5) PCDATA $0, $1
0x0024 00036 (recover/compile.go:5) PCDATA $1, $0
0x0024 00036 (recover/compile.go:5) LEAQ ..fp+40(SP), AX
0x0029 00041 (recover/compile.go:5) PCDATA $0, $0
0x0029 00041 (recover/compile.go:5) MOVQ AX, (SP)
0x002d 00045 (recover/compile.go:5) CALL runtime.gorecover(SB)
我们可以看到 recover() 函数调用被替换成了 runtime.gorecover() 函数。runtime.gorecover() 实现源码位于 src/runtime/panic.go。
gorecover()
runtime.gorecover() 函数实现很简短:
func gorecover(argp uintptr) interface{} {gp := getg()
// 获取 panic 实例,只有发生了 panic,实例才不为 nil
p := gp._panic
// recover 限制条件
if p != nil !p.goexit !p.recovered argp == uintptr(p.argp) {
p.recovered = true
return p.arg
return nil
}
短短的代码,蕴含的信息量却很大。它可以解释以下问题:
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。
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),传入函数 B 的参数地址
// argp == uintptr(p.argp) 检测失败,无法恢复
if err := recover(); err != nil {
fmt.Println(A)
}
设计思路
通过以上源码的分析,我们可以很好地回答以下问题了:
为什么 recover() 一定要在 defer() 函数中才生效?
假如 defer() 函数中调用了函数 A(),为什么 A() 中的 recover() 不能生效?
如果 recover() 不在 defer() 函数中,那么 recover() 可能出现在 panic() 之前,也可能出现在 panic() 之后,出现在 panic() 之前,因为找不到 panic 实例而无法生效,出现在 panic() 之后,代码没有机会执行,所以 recover() 必须存在于 defer 函数中才会生效。
通过上面的分析,从代码层面我们理解了为什么 recover() 函数必须被 defer 直接调用才会生效。但为什么要有这样的设计呢?
笔者也没有找到官方关于此设计的资料,不过笔者认为此设计非常合理。
考虑下面的代码:
func foo() {defer func() {thirdPartPkg.Clean() // 调用第三方包清理资源
if err != nil { // 条件不满足触发 panic
panic(xxx)
}
有时我们会在代码里显式地触发 panic,同时往往还会在 defer 函数里调用第三方包清理资源,如果第三方包也使用了 recover(),那么我们触发的 panic 将会被拦截,而且这种拦截可能是非预期的,并不我们期望的结果。
“Go 编程中 recover 源码是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!