recover 有哪些限制?为什么它只能“救”当前 goroutine?
结论先行
recover 要截断 panic,需要三个前提同时满足:① 必须处于 defer 函数的执行路径中(语言规范要求由被延迟执行的函数直接调用);② 必须在发生 panic 的同一个 goroutine 内;③ 调用时确实处于 panic 展开过程。panic 状态按 goroutine 隔离,所以子 goroutine 的 panic 无法被父 goroutine 捕获,只会让整个进程崩溃。
要点
- 基本机制:panic 沿当前 goroutine 的调用栈逐层展开,沿途执行 defer;recover 返回 panic 传入的值并把展开“刹车”。
- 限制一(位置):defer 之外调用 recover 恒返回 nil;规范表述是“由被延迟执行的函数直接调用”,不要把间接封装当成受支持用法。
- 限制二(goroutine):panic 状态挂在发生 panic 的那个 goroutine 上,别的 goroutine 的 defer/recover 感知不到。
- 推论:凡自建 goroutine(协程池 worker、后台任务),入口都要自带 defer+recover 兜底,否则一个 panic 干掉整个进程。
- 限制三(时机):没有 panic 时 recover 返回 nil;Go 1.21 起 panic(nil) 会让 recover 返回 *runtime.PanicNilError,语义有变化。
- recover 之后:通常先记录日志(可用 runtime/debug.Stack 保留现场),再决定继续、重试或 panic(r) 重新抛出。
- 常见误区:把 recover 写在普通函数里“等 panic 来了再恢复”,永远等不到——它必须挂在 defer 里。
示例
func run(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r) // panic 转 error
}
}()
fn()
return nil
}
// 子 goroutine 必须自救:父 goroutine 的 recover 管不到它
go func() {
defer func() { recover() }() // 每个 goroutine 自己兜底
doRisky()
}()
常见追问/记忆点
- 追问:父 goroutine 能捕获子 goroutine 的 panic 吗?答:不能,panic 只沿自身调用栈展开,子 goroutine 要在自己入口兜底。
- 追问:recover 后想保留崩溃现场怎么办?答:调 runtime/debug.Stack() 记日志,必要时 panic(r) 重新抛出。
- 追问:defer 里 recover 与直接 recover 有区别吗?答:不写在 defer 里根本捕获不到,这是使用前提而非风格问题。
- 记忆:defer 内、同 goroutine、正在 panic,三点缺一不可。