5.4 panic and recover
Golang 中的panic
和recover
:
panic
能够改变程序的控制流,调用panic
后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的defer
recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥作用
5.4.1 现象
使用panic
和recover
时,会有:
panic
只会触发当前 Goroutine 的defer
recover
只有在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