189 8069 5689

Go编程中recover源码是什么

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


网站标题:Go编程中recover源码是什么
当前链接:http://cdxtjz.com/article/jpjjde.html

其他资讯