温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Go编程中recover源码是什么

发布时间:2022-01-07 15:28:20 来源:亿速云 阅读:137 作者:iii 栏目:云计算

本篇内容介绍了“Go编程中recover源码是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

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源码是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI