panic、recover 与 Goexit 都会触发 defer,但行为各有不同。这篇用真实程序逐个验证:展开时 defer 的顺序、recover 到底在哪里有效、Goexit 与 panic 的差别、panic(nil) 的变化,以及未被 recover 的 panic 打印什么。
调用链 level1 → level2 → level3,level3 panic,只有 level1 里 recover:
level3:即将 panic
level3 的 defer
level2 的 defer
level1 的 defer:recover 之后继续运行
level1 返回的 err = recover 到:boompanic 发生后,运行时沿着调用栈从最内层往外执行每个函数的 defer,直到有一个 defer 里的 recover() 接住它;那个函数随后正常返回给自己的调用者。中间那些函数里 panic 点之后的语句不会执行。
defer 里调用 helper() 里的 recover → <nil>(没有接住)
nestedRecover() = "外层 defer 才接住:p1"
deferRecoverDirect() = "外层 defer 接住了:p2"语言规范要求 recover 必须由被 defer 的函数直接调用才有效。实验里在 defer 的函数里调用 helper()、helper 里再 recover(),得到 nil;写成 defer recover() 也不起作用。常见的错误就是把 recover 包进一个通用的 helper。
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) 之后 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 1Go 1.21 起 panic(nil) 会变成 *runtime.PanicNilError,使 recover() 的 nil 只表示“没有 panic”。除以零、下标越界等运行时错误实现了 runtime.Error 接口,可以用类型断言区分。
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。上面实验直接用环境变量切换,验证了两种结果。