Go 语言中两个经常成对出现的两个关键字 — panic
和 recover
。这两个关键字与上一节提到的 defer
有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。
panic
能够改变程序的控制流,调用panic
后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的defer
;recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥作用;
现象
panic
只会触发当前 Goroutine 的defer
;recover
只有在defer
中调用才会生效;panic
允许在defer
中嵌套多次调用;
跨协程失效
panic
只会触发当前 Goroutine 的延迟函数调用。
func main() {
defer println("in main")
go func() {
defer println("in goroutine")
panic("")
}()
time.Sleep(1 * time.Second)
}
复制代码
$ go run main.go
in goroutine
panic:
...
复制代码
运行这段代码时会发现 main
函数中的 defer
语句并没有执行,执行的只有当前 Goroutine 中的 defer
。
前面曾经介绍过 defer
关键字对应的 runtime.deferproc
会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。
多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic
时也不应该执行其他 Goroutine 的延迟函数。
失效的崩溃恢复
在主程序中调用 recover
试图中止程序的崩溃,但是从运行的结果中能看出,下面的程序没有正常退出。
func main() {
defer fmt.Println("in main")
if err := recover(); err != nil {
fmt.Println(err)
}
panic("unknown err")
}
复制代码
$ go run main.go
in main
panic: unknown err
goroutine 1 [running]:
main.main()
...
exit status 2
复制代码
recover
只有在发生 panic
之后调用才会生效。然而在上面的控制流中,recover
是在 panic
之前调用的,并不满足生效的条件,所以需要在 defer
中使用 recover
关键字。
嵌套奔溃
Go 语言中的 panic
是可以多次嵌套调用的,如下所示的代码就展示了如何在 defer
函数中多次调用 panic
。
func main() {
defer fmt.Println("in main 1")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
defer fmt.Println("in main 2")
panic("panic once")
}
复制代码
$ go run main.go
in main 2
in main 1
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
...
exit status 2
复制代码
程序多次调用 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;
近期评论