【Go进阶—基础特性】panic 和 recover

panic 和 recover 也是常用的关键字,这两个关键字与上一篇提到的 defer 联系很紧密。用一句话总结就是:调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;而 recover 可以中止 panic 造成的程序崩溃,不过它只能在 defer 中发挥作用。
panic
panic 是一个内置函数,接受一个任意类型的参数,参数将在程序崩溃时打印出来,如果被 recover 恢复的话,该参数也是 recover 的返回值。panic 可以由程序员显式触发,运行时遇到意料之外的错误如内存越界时也会触发。
在上一篇中我们知道每个 Goroutine 都维护了一个 _defer 链表(非开放编码情况下),执行过程中每遇到一个 defer 关键字都会创建一个 _defer 实例插入链表,函数退出时一次取出这些 _defer 实例并执行。panic 发生时,实际上是触发了函数退出,也即把执行流程转向了 _defer 链表。
panic 的执行过程中有几点需要明确:

  • panic 会递归执行当前 Goroutine 中所有的 defer,处理完成后退出;
  • panic 不会处理其他 Goroutine 中的 defer;
  • panic 允许在 defer 中多次调用,程序会终止当前 defer 的执行,继续之前的流程。
数据结构 panic 关键字在 Go 语言中是由数据结构 runtime._panic 表示的。每当我们调用 panic 都会创建一个如下所示的数据结构:
type _panic struct { argpunsafe.Pointer arginterface{} link*_panic recovered bool abortedbool goexitbool }

  • argp 是指向 defer 函数参数的指针;
  • arg 是调用 panic 时传入的参数;
  • link 指向前一个_panic 结构;
  • recovered 表示当前 _panic 是否被 recover 恢复;
  • aborted 表示当前的 _panic 是否被终止;
  • goexit 表示当前 _panic 是否是由 runtime.Goexit 产生的。
_panic 链表与 _defer 链表一样,都是保存在 Goroutine 的数据结构中:
type g struct { // ... _panic*_panic _defer*_defer // ... }

执行过程 编译器会将关键字 panic 转换成 runtime.gopanic 函数,我们来看一下它的核心代码:
func gopanic(e interface{}) { gp := getg() ... var p _panic// 创建新的 _panic 结构 p.arg = e// 存储 panic 的参数 p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 这两行是将新结构插入到当前 Goroutine 的 panic 链表头部for { d := gp._defer // 开始遍历 _defer 链表 if d == nil { break }// 嵌套 panic 的情形 if d.started { if d._panic != nil { d._panic.aborted = true // 标记之前 _defer 中的 _panic 为已终止 } // 从链表中删除本 defer d._panic = nil if !d.openDefer { d.fn = nil gp._defer = d.link freedefer(d) continue } }d.started = true // 标记 defer 已经开始执行d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 标记触发 defer 的 _panicreflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) // 执行 defer 函数,省略对开放编码 _defer 的额外处理d._panic = nil d.fn = nil gp._defer = d.linkpc := d.pc sp := unsafe.Pointer(d.sp) freedefer(d) // 如果被 recover 恢复的话,处理下面的逻辑 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") // mcall should not return } }fatalpanic(gp._panic) // 终止整个程序 *(*int)(nil) = 0 }

该函数的执行过程包含以下几个步骤:
  1. 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;
  2. 判断是否是嵌套 panic 的情形,进行相关标记和处理;
  3. 不断从当前 Goroutine 的 _defer 链表中获取 _defer 并调用 runtime.reflectcall 运行延迟调用函数;
  4. 调用 runtime.fatalpanic 中止整个程序。
recover
recover 也是一个内置函数,用于消除 panic 并使程序恢复正常。recover 的执行过程也有几点需要明确:
  • recover 的返回值就是消除的 panic 的参数;
  • recover 必须直接位于 defer 函数内(不能出现在另一个嵌套函数中)才能生效;
  • recover 成功处理异常后,函数不会继续处理 panic 之后的逻辑,会直接返回,对于匿名返回值将返回相应的零值。
执行过程 编译器会将关键字 recover 转换成 runtime.gorecover:
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }

函数的实现很简单,获取当前 Goroutine 中的 _panic 实例,在符合条件的情况下将 _panic 实例的 recovered 状态标记为 true,然后返回 panic 函数的参数。
【【Go进阶—基础特性】panic 和 recover】我们来看一下 recover 的几个生效条件:
  • p != nil:必须存在 panic;
  • !p.goexit:非 runtime.Goexit();
  • !p.recovered:还未被恢复;
  • argp == uintptr(p.argp):recover 必须在 defer 中直接调用。
首先,必须存在 panic,runtime.Goexit() 产生的 panic 无法被恢复,这些没什么好说的。假设函数包含多个 defer,前面的 defer 通过 recover 消除 panic 后,剩余 defer 中的 recover 不能再次恢复。
有一点会让人感到疑惑,recover 函数没有参数,为什么 gorecover 函数却有参数?这正是为了限制 recover 必须在 defer 中被直接调用。gorecover 函数的参数为调用 recover 函数的参数地址,_panic 结构中保存了当前 defer 函数的参数地址,如果二者一致,说明 recover 是在 defer 中被直接调用。示例如下:
func test() { defer func() { // func A func() { // func B // gorecover 的参数 argp 为 B 的参数地址,p.argp 为 A 的参数的指针 // argp != p.argp,无法恢复 if err := recover(); err != nil { fmt.Println(err) } }() }() }

    推荐阅读