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,三点缺一不可。
笔记加载中…