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 的执行,继续之前的流程。
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 产生的。
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
}
该函数的执行过程包含以下几个步骤:
- 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;
- 判断是否是嵌套 panic 的情形,进行相关标记和处理;
- 不断从当前 Goroutine 的 _defer 链表中获取 _defer 并调用 runtime.reflectcall 运行延迟调用函数;
- 调用 runtime.fatalpanic 中止整个程序。
recover 也是一个内置函数,用于消除 panic 并使程序恢复正常。recover 的执行过程也有几点需要明确:
- recover 的返回值就是消除的 panic 的参数;
- recover 必须直接位于 defer 函数内(不能出现在另一个嵌套函数中)才能生效;
- recover 成功处理异常后,函数不会继续处理 panic 之后的逻辑,会直接返回,对于匿名返回值将返回相应的零值。
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 中直接调用。
有一点会让人感到疑惑,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)
}
}()
}()
}