panic 是什么样子的
panic 会中止当前函数,执行它的 defer 调用,然后在调用方中做同样的事,沿着调用栈一路向上。如果到达 goroutine 的顶层,程序就会崩溃。
这个程序以状态码 2 退出。输出如下:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
被 defer 的调用在崩溃报告之前执行了。调用栈会写明 goroutine、函数和行号,通常足以找到 bug。
常见的运行时 panic
| 错误信息 | 原因 |
|---|---|
index out of range [5] with length 3 | slice、数组或字符串的索引超出末尾 |
slice bounds out of range [:7] with capacity 5 | 切片超出容量 |
invalid memory address or nil pointer dereference | 通过 nil 指针读取字段或调用方法 |
assignment to entry in nil map | 写入一个从未创建的 map |
interface conversion: interface {} is int, not string | 单返回值的类型断言断言成了错误的类型 |
integer divide by zero | 整数除以 0 或对 0 取模(浮点数则得到 +Inf 或 NaN) |
close of closed channel、send on closed channel | channel 使用不当 |
all goroutines are asleep - deadlock! | 所有 goroutine 都被阻塞(这是致命错误,不是 panic) |
这些都是程序中的 bug,而不是需要处理的情况。修复方法是加上边界检查、nil 检查、一次 make,或使用 comma-ok 形式的断言,而不是 recover。
恢复
recover() 可以拦住 panic。它只有在 defer 函数中被直接调用时才有效,因为在 panic 展开调用栈期间,只有 defer 函数会执行。
输出:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
第二次调用中发生了什么:
a / b引发了 panic。- 被 defer 的闭包执行,
recover()返回 panic 的值(一个runtime.Error)。 - 栈展开停止。
safeDivide正常返回到main,命名返回值err已被闭包设置好。
正是命名返回值让 defer 函数能够把一个 error 交回去。没有命名返回值的话,函数返回的是各返回值的零值。defer 页面讲解了被 defer 的闭包如何修改返回值。
没有 panic 时 recover() 返回 nil,所以 if r != nil 检查让 defer 函数在正常路径上不产生任何影响。在 defer 函数之外调用,或者在被 defer 函数调用的另一个函数中调用,recover 都返回 nil,什么也不做。
用自己的值调用 panic
panic 可以接收任何值,通常是一个 error 或字符串。
只恢复你预期的 panic,其他的重新 panic。吞掉所有 panic 会掩盖真正的 bug。
从 Go 1.21 起,panic(nil) 会被转换成 *runtime.PanicNilError,所以现在 recover() 返回 nil 就可以可靠地表示“没有 panic”。
goroutine 中的 panic
recover 只能捕获它自己所在 goroutine 中的 panic。任何一个没有 recover 的 goroutine 发生 panic,都会让整个进程退出,包括 main 和所有其他 goroutine。
两行 worker 的输出顺序不固定;main finished 总是最后出现。main 中的 defer recover() 救不了第二个 worker 引发的崩溃。这就是 HTTP 服务器按请求进行 recover 的原因:net/http 会恢复每个处理器 goroutine 中的 panic,记录日志并关闭那个连接,这样一个有问题的请求不会拖垮整个服务器。
有些故障是致命错误而不是 panic,根本无法恢复:concurrent map writes、内存耗尽,以及死锁检测器报出的 all goroutines are asleep。
什么时候主动 panic 是对的
Go 的一般原则是:运行时可能出错的任何情况都返回 error,只有程序员的错误才 panic。具体来说,以下情况适合 panic:
- 不变式被破坏。 对你自己定义的枚举做
switch,却走到了一个不可能出现的分支。继续执行会破坏数据。 Must辅助函数收到错误的常量输入。regexp.MustCompile、template.Must和uuid.MustParse包装了一个返回 error 的函数,失败时就 panic。把它们用于编译时就已知的值,通常是包级变量,此时失败意味着源代码本身写错了:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- 启动无法继续。
main中缺少必需的配置。即便如此,打印错误并调用os.Exit(1)往往也比输出一段调用栈更干净。
以下情况不该用 panic:
- 预期之内的失败:无效的用户输入、缺失的文件、超时。返回一个
error;参见错误处理。 - 控制流程:在一棵庞大的调用树中把 panic 和 recover 当作异常来用,会让代码难以理解。标准库在少数几个地方内部这样做(
encoding/json的编码器),但总会在返回前 recover,所以不会有 panic 泄漏出这个包。 - 库的 API:一个遇到错误输入就 panic 的库,会迫使每个调用方都加上 recover。应该返回 error。
常见错误
- 在 defer 函数之外调用 recover。 它会返回
nil。 - 在
main中 recover 来处理某个 goroutine 的 panic。 每个 goroutine 都需要自己的 recover。 - 吞掉所有 panic。 连同调用栈一起记录日志(
runtime/debug中的debug.Stack()),并对预期之外的 panic 重新 panic。 - 用
recover来处理 nil map 写入或索引越界。 应该去修复 bug。
常见问题
Go 中的 panic 是什么?
一种运行时故障,它会中止当前 goroutine 的正常执行流程。Go 会从最内层向外,依次执行调用栈上每个函数中被 defer 的调用;如果没有任何地方 recover,程序会打印 panic 的值和调用栈,然后以状态码 2 退出。panic 来自 bug(索引越界、空指针解引用、写入 nil map),或者来自显式的 panic(v) 调用。
Go 中如何从 panic 中恢复?
在 defer 函数中调用 recover():defer func() { if r := recover(); r != nil { ... } }()。它返回传给 panic 的值并停止栈展开,于是注册这个 defer 的函数会正常返回给它的调用方。在其他任何地方调用,recover 都返回 nil,什么也不做。
能在一个 goroutine 中恢复另一个 goroutine 的 panic 吗?
不能。recover 只能拦住它所在 goroutine 中的 panic。你启动的 goroutine 如果发生 panic,而该 goroutine 内部没有 recover,整个程序都会崩溃。每个可能 panic 的 goroutine 都需要自己的 defer recover。
什么时候该用 panic,而不是返回 error?
用于 bug 和不可能出现的状态,而不是预期之内的失败。错误的输入、缺失的文件和网络错误都应该是 error。当不变式被破坏、Must 辅助函数收到一个本应总是合法的常量(regexp.MustCompile),或者程序根本无法启动时,panic 是合理的。库不应该让 panic 泄漏到它的公开 API 之外。