您好,登录后才能下订单哦!
本篇内容介绍了“Go编程中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。
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源码是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。