bigywater.tech
Go · 33

panic、recover 与 runtime.Goexit:栈展开时 defer 的执行顺序

panic、recover 与 Goexit 都会触发 defer,但行为各有不同。这篇用真实程序逐个验证:展开时 defer 的顺序、recover 到底在哪里有效、Goexit 与 panic 的差别、panic(nil) 的变化,以及未被 recover 的 panic 打印什么。

从内到外
panic 展开时 defer 的执行顺序:level3 → level2 → level1
直接调用
recover() 必须由被 defer 的函数直接调用,helper 里调用或 defer recover() 都不接住
GODEBUG=panicnil=1
panic(nil) 的 recover 结果:默认是 PanicNilError,设置后是 nil

展开顺序

实测 · 本机执行输出
  调用链 level1 → level2 → level3,level3 panic,只有 level1 里 recover:
    level3:即将 panic
    level3 的 defer
    level2 的 defer
    level1 的 defer:recover 之后继续运行
    level1 返回的 err = recover 到:boom

panic 发生后,运行时沿着调用栈从最内层往外执行每个函数的 defer,直到有一个 defer 里的 recover() 接住它;那个函数随后正常返回给自己的调用者。中间那些函数里 panic 点之后的语句不会执行。

recover 的位置

实测 · 本机执行输出
    defer 里调用 helper() 里的 recover → <nil>(没有接住)
    nestedRecover() = "外层 defer 才接住:p1"
    deferRecoverDirect() = "外层 defer 接住了:p2"

语言规范要求 recover 必须由被 defer 的函数直接调用才有效。实验里在 defer 的函数里调用 helper()、helper 里再 recover(),得到 nil;写成 defer recover() 也不起作用。常见的错误就是把 recover 包进一个通用的 helper。

Goexit

实测 · 本机执行输出
    Goexit 期间 recover() = <nil>;defer 正常执行

runtime.Goexit 终止当前 goroutine,但会先执行所有 defer。它不是 panic,所以 defer 里的 recover() 返回 nil。在 main 里调用它,等其它 goroutine 都结束后程序以 fatal error: no goroutines (main called runtime.Goexit) - deadlock! 终止。

panic(nil) 与运行时错误

实测 · 本机执行输出
    panic(nil) 之后 recover() = runtime error: panic called with nil argument;类型 *runtime.PanicNilError;是 *runtime.PanicNilError?true
    除以 0:recover() = runtime error: integer divide by zero;类型 runtime.errorString;实现 runtime.Error?true
    越界:recover() = runtime error: index out of range [5] with length 1

Go 1.21 起 panic(nil) 会变成 *runtime.PanicNilError,使 recover() 的 nil 只表示“没有 panic”。除以零、下标越界等运行时错误实现了 runtime.Error 接口,可以用类型断言区分。

没接住的 panic

实测 · 本机执行输出
  defer 里再 panic(没有 recover):退出码 2
    panic: 第一个 panic
    (缩进)panic: 第二个 panic(在 defer 里)
  第一个 panic 被 recover,随后 defer 里又 panic:退出码 2
    panic: 第二个 panic
  recover 之后重新 panic:退出码 2
    panic: 原始 panic [recovered]
    (缩进)panic: 重新 panic:原始 panic
  main 里 recover,panic 发生在另一个 goroutine:退出码 2
    panic: 另一个 goroutine 的 panic
  main 里调用 runtime.Goexit:退出码 2
    main 的 defer 执行了
    另一个 goroutine 正常结束
    fatal error: no goroutines (main called runtime.Goexit) - deadlock!

panic 在 defer 里再 panic、或 recover 后重新 panic,打印时会形成链(后面的缩进、被接住的标 [recovered]);recover 只对本 goroutine 有效,另一个 goroutine 的 panic 会让整个进程以退出码 2 结束。所以起 goroutine 时要在 goroutine 内部自己 recover。

旧行为的开关

实测 · 本机执行输出
  默认:退出码 0;recover() = runtime error: panic called with nil argument
  GODEBUG=panicnil=1:退出码 0;recover() = <nil>

godebug 文档说明 panicnil 是 Go 1.21 引入的设置,go.mod 里写 go 1.20 时默认就是 panicnil=1。上面实验直接用环境变量切换,验证了两种结果。

分步演示

panic、recover 与 Goexit
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个“没被 recover”的情形都是同一个程序用不同命令行参数起的独立进程,退出码与输出来自真实进程;读过的资料:doc/godebug.md 里关于 panicnil 的说明
    • 没有验证:panic 与 recover 的运行时实现(栈展开的数据结构)、defer 链里多个 recover 的交互(只试了一个)、cgo 回调中的 panic、signal 触发的 panic(如 nil 指针解引用)的细节、Goexit 在 init 函数里的行为
    京ICP备2026067502号-1京公网安备11010502063487号