Menu

Go panic 与 recover:运行时 panic 与何时该用 panic

panic 会中止正常执行并展开调用栈,同时执行被 defer 的调用。了解哪些情况会引发 panic,在 defer 函数中调用 recover 如何拦住它,你会遇到的运行时错误信息,以及什么时候主动 panic 才是正确的选择。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 3slice、数组或字符串的索引超出末尾
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 channelchannel 使用不当
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

第二次调用中发生了什么:

  1. a / b 引发了 panic。
  2. 被 defer 的闭包执行,recover() 返回 panic 的值(一个 runtime.Error)。
  3. 栈展开停止。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 之外。

Coddy 编程语言插图

和 Coddy 一起学编程

开始