本次主要聊聊 Go 语言中关于 panic 和 recover 搭配使用 ,以及 panic 的基本原理
最近工作中审查代码的时候发现一段代码,类似于如下这样,将 recover 放到一个子协程里面,期望去捕获主协程的程序异常
看到此处,是否会想这段代码在项目中是想当然写出来的吧,然而平日中,大多问题是出现在认知偏差上,那么本次,我们就来消除一下这个认知偏差
关于 Go 语言中显示的使用 panic 的地方不多,一般 panic ,基本上会出现在咱们程序出现异常退出的时候
例如访问了空指针里面的值,则会 panic 报错无效的内存地址,又例如访问量数组中不存在的数组所索引,或者切片索引,那么会报错 panic 数组越界等等
可是碰到这些 panic 的时候,实际上我们并不期望当前的服务直接挂掉,而是期望这个异常能够被识别,且不影响程序其他部分的模块运行
在 Go 中可以将 defer 和 recover 进行搭配使用,可以捕获和处理大部分的异常情况,例如可以这样
这里可以看到,recover 捕获异常和发生异常的部分是在同一个协程中,实验证明是可以正常捕获并且处理异常
func main() {
log.SetFlags(log.Lshortfile)
panic("panic coming...")
}
func main() {
log.SetFlags(log.Lshortfile)
if err := recover(); err != nil {
log.Println("recover panic : ", err)
}
panic("panic coming...")
}
自然 recover 函数是在 panic 调用之前就已经执行,此时是还没有异常需要捕获和恢复的,待程序运行到 panic 处的时候,实际上并没有没有处理程序崩溃的异常
结果,仍然是程序崩溃
看了上述现象,实际上还是对知识点理解得不够,使用的时候想当然了,就像使用 defer 一样,如果对他不够了解的话,使用的时候,确实会出现一些奇奇怪怪的现象,对于 defer 的使用可以查看文末的文章地址
builtin\builtin.go
中可以看到注释注释中有说关于 panic 和 recover 的使用是作用于当前协程的,因此我们使用的时候,如果跨协程教程使用,自然不会达到我们期望的效果
runtime\runtime2.go
_panic 的结构如下:
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
pc uintptr
sp unsafe.Pointer
recovered bool
aborted bool
goexit bool
}
上述两个结构表达的意思是,程序中出现 panic 的时候,实际上都会创建一个 _panic 结构,这个 _panic 结构里面存储了当前程序崩溃的一些必要信息,如下:
是一个 unsafe.Pointer 类型的成员,指向 defer 调用参数的指针
出现 panic 的原因,如果我们显示调用 panic,那么就是我们填入 panic 函数中的参数,例如上述的 panic coming ...
是一个指针,指向上一个,最近的一个 _panic 结构的地址,实际上此处就可以看到这个指针对应的是一个链表,一个又多个 _panic 结构组成的链表
panic 是否已经处理完毕,即当前的这个 panic 是否是已经被 recover 了
表示当前的 panic 是否被中止
我们知道运行函数的时候需要入栈,运行完毕之后需要出栈
那么我们继续来阅读源码,上述看到 sp 和 pc ,那么我们就简单写一个 panic 的代码来看看汇编到底是怎么执行的,不用担心看不懂,我们只需要看关键词就行
还是上面的程序
程序运行的时候可以执行 go tool compile -S main.go
可以看到汇编代码,可能其他的看不懂,但是我们可以看到如下关键词
代码中可以看到 p.recovered
逻辑下的关于 recover 的逻辑被删除掉了,在文章的后面会继续说到,当前我们先关注 panic 的事项
runtime.gopanic 程序的逻辑大体是这样的
panic coming ...
信息Xdm 可以看上图,自己捋一捋逻辑就清晰了
接着,我们来看
通过 runtime.gopanic 我们可以看到 fatalpanic 函数基本上就是做一个收尾工作了,如果上述程序处理完毕之后, fatalpanic 校验到 panic 是需要 recover 的,那么就打印 [recovered]
打印的这个信息是由 上图中 printpanics 完成的
这下知道 panic 是如何去执行的了,那么对于现在来研究 recover 是如何落实的
还是同一个例子,咱们将 defer 部分的代码注打开,来继续看看效果
func main() {
log.SetFlags(log.Lshortfile)
defer func() {
if err := recover(); err != nil {
log.Println("recover panic : ", err)
}
}()
panic("panic coming...")
}
自然效果是我们期望的,捕获到了异常,且处理了
继续打印汇编来查看一下关键词,是否有我们期望的函数出现
此处我们可以看到,实际 Go 中调用了多个函数
自然明眼人都看的出现,关键的函数实现自然是 runtime.gorecover ,那么我们来一探究竟
查看源码我们可以知道, runtime.gorecover 实际上就是根据当前协程的 _panic 结构数据来判断是否需要恢复,如果需要则将 p.recovered = true
自然在这里将当前协程的数据修改掉,正是为了后续执行 runtime.gopanic 的时候提供保障, runtime.gopanic 执行的时候就会去判断和处理这个 p.recovered
前文中提到的关于 runtime.gopanic 中 处理 p.recovered
的逻辑是这样的
p.recovered
设置是否恢复p.recovered
已处理,则执行 recovery 函数因此,当我们在同一个协程中出现了 panic,且在同一个协程中去使用 defer 来配合 recover 来进行捕获异常和处理异常,就可以得以实现,看到这里,有没有觉得还是蛮简单的,不就是去对一个 p.recovered
进行配合处理吗
自然,表面上是这样,其中对于寄存器的各种数据处理涉及的内容还是不少的,不过这不在我们今天聊的范畴中了
至此,相信你已经知道了这些
当然,如果文章对你有帮助的话,欢迎留言评论哦
感谢阅读,欢迎交流,点个赞,关注一波 再走吧
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~
文中提到的技术点,感兴趣的可以查看这些文章:
可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI