5.4 panic and recover
Golang 中的panic和recover:
panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的deferrecover可以中止panic造成的程序崩溃。它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用

5.4.1 现象
使用panic和recover时,会有:
panic只会触发当前 Goroutine 的deferrecover只有在defer中调用才会生效panic允许在defer中嵌套多次调用
跨协程失效
panic 只会触发当前 Goroutine 的延迟函数调用:
func main() {
defer println("defer in main")
go func() {
defer println("defer in goroutine")
panic("panic in goroutine")
}()
time.Sleep(time.Second)
}
$ go run main.go
defer in goroutine
panic: panic in goroutine
goroutine 5 [running]:
main.main.func1()
可以看到main函数中的defer没有被触发,只有panic所在的goroutine的defer被触发。
因为runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联,所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数。

失效的recover
recover只有在defer中才会生效:
func main() {
defer println("defer in main")
if err := recover(); err != nil {
println(err)
}
panic("panic in main")
}
$ go run main.go
defer in main
panic: panic in main
goroutine 1 [running]:
main.main()
上述代码因为recover在panic调用之前被调用了,不会生效。需要在defer中才会生效。
panic嵌套
panic可以嵌套:
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
panic("panic once")
}
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
5.4.2 数据结构
panic的数据结构由runtime._panic表示:
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
pc uintptr
sp unsafe.Pointer
goexit bool
}
argp:指向defer调用时参数的指针arg:调用panic的参数link:指向更早调用的panic,形成链表recovered:表示当前panic是否被恢复aborted:表示当前panic是否被终止
5.4.3 panic
panic 终止程序的实现原理,编译器会将关键字 panic 转换成 runtime.gopanic,函数主要流程:
- 创建新的
runtime._panic并添加到所在 Goroutine 的_panic链表的最前面 - 在循环中不断从当前 Goroutine 的
_defer中链表获取runtime._defer并调用runtime.reflectcall运行延迟调用函数 - 调用
runtime.fatalpanic中止整个程序
func gopanic(e interface{}) {
gp := getg()
...
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
d := gp._defer
if d == nil {
break
}
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
if p.recovered {
...
}
}
fatalpanic(gp._panic)
*(*int)(nil) = 0
}
runtime.fatalpanic 实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanics 打印出全部的 panic 消息以及调用时传入的参数:
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
if startpanic_m() && msgs != nil {
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs)
}
if dopanic_m(gp, pc, sp) {
crash()
}
exit(2)
}
打印崩溃消息后会调用 runtime.exit 退出当前程序并返回错误码 2,程序的正常退出也是通过 runtime.exit 实现的。
5.4.4 recover
编译器会将关键字 recover 转换成 runtime.gorecover:
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
- 若当前 Goroutine 没有调用
panic,那么该函数会直接返回nil - 若调用了
panic,修改runtime._panic的recovered字段
runtime.gorecover 函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanic 函数负责:
func gopanic(e interface{}) {
...
for {
// 执行延迟调用函数,可能会设置 p.recovered = true
...
pc := d.pc
sp := unsafe.Pointer(d.sp)
...
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}
}
...
}
runtime.recovery 在调度过程中会将函数的返回值设置成 1。
当 runtime.deferproc 函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn:
func deferproc(siz int32, fn *funcval) {
...
return0()
}
跳转到 runtime.deferreturn 函数之后,程序就已经从 panic 中恢复了并执行正常的逻辑,而 runtime.gorecover 函数也能从 runtime._panic 结构中取出了调用 panic 时传入的 arg 参数并返回给调用方。
5.4.5 小结
panic和recover的流程如下:
- 编译器会负责做转换关键字的工作:
- 将
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
