Go 语言中两个经常成对出现的两个关键字 — panic 和 recover。这两个关键字与上一节提到的 defer 有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。
panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的defer;recover可以中止panic造成的程序崩溃。它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用;
现象
panic只会触发当前 Goroutine 的defer;recover只有在defer中调用才会生效;panic允许在defer中嵌套多次调用;
跨协程失效
panic 只会触发当前 Goroutine 的延迟函数调用。
1 | go复制代码func main() { |
1 | go复制代码$ go run main.go |
运行这段代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。
前面曾经介绍过 defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。
多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic 时也不应该执行其他 Goroutine 的延迟函数。
失效的崩溃恢复
在主程序中调用 recover 试图中止程序的崩溃,但是从运行的结果中能看出,下面的程序没有正常退出。
1 | go复制代码func main() { |
1 | less复制代码$ go run main.go |
recover 只有在发生 panic 之后调用才会生效。然而在上面的控制流中,recover 是在 panic 之前调用的,并不满足生效的条件,所以需要在 defer 中使用 recover 关键字。
嵌套奔溃
Go 语言中的 panic 是可以多次嵌套调用的,如下所示的代码就展示了如何在 defer 函数中多次调用 panic。
1 | go复制代码func main() { |
1 | go复制代码$ go run main.go |
程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。
小结
分析程序的崩溃和恢复过程比较棘手,代码不是特别容易理解。
- 编译器会负责做转换关键字的工作;
- 将
panic和recover分别转换成runtime.gopanic和runtime.gorecover; - 将
defer转换成runtime.deferproc函数; - 在调用
defer的函数末尾调用runtime.deferreturn函数;
- 将
- 在运行过程中遇到
runtime.gopanic方法时,会从 Goroutine 的链表依次取出runtime._defer结构体并执行; - 如果调用延迟执行函数时遇到了
runtime.gorecover就会将_panic.recovered标记成 true 并返回panic的参数;- 在这次调用结束之后,
runtime.gopanic会从runtime._defer结构体中取出程序计数器pc和栈指针sp并调用runtime.recovery函数进行恢复程序; runtime.recovery会根据传入的pc和sp跳转回runtime.deferproc;- 编译器自动生成的代码会发现
runtime.deferproc的返回值不为 0,这时会跳回runtime.deferreturn并恢复到正常的执行流程;
- 在这次调用结束之后,
- 如果没有遇到
runtime.gorecover就会依次遍历所有的runtime._defer,并在最后调用runtime.fatalpanic中止程序、打印panic的参数并返回错误码 2;
参阅
本文转载自: 掘金