recover()必须在defer中调用,且仅对同一goroutine内最近一次panic生效;跨goroutine调用返回nil,需在子goroutine内封装defer recover并结合debug.Stack()记录堆栈与上下文。

为什么 recover() 必须在 defer 中调用
协程(goroutine)里 panic 发生后,若没被 recover,会直接终止该 goroutine 并丢失堆栈——recover() 只有在 defer 函数中调用才有效,且必须在 panic 发生的同一 goroutine 内。跨 goroutine 调用 recover() 永远返回 nil。
常见错误是把 recover() 放在主 goroutine 的 defer 里,试图捕获子 goroutine 的 panic,这完全无效。
-
recover()不是全局钩子,只对当前 goroutine 的最近一次 panic 生效 - 必须确保
defer注册发生在 panic 可能发生之前(即在 goroutine 启动函数内部) - 如果 defer 函数本身 panic,且未再 recover,会导致 goroutine 彻底崩溃
标准 recover 模板:带堆栈和上下文的捕获
单纯 recover() 只拿到 panic 值,无法定位问题。要真正“规范化”,需结合 runtime/debug.Stack() 和日志上下文(如 goroutine ID、时间戳、关键参数)。
示例写法:
立即学习“go语言免费学习笔记(深入)”;
go func() {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("panic recovered in goroutine %d: %+v\n%s",
time.Now().UnixMilli(), r, stack)
}
}()
// 业务逻辑,可能 panic
riskyOperation()
}()- 避免直接
fmt.Println(recover())—— 无堆栈、无时间、无 goroutine 标识 - 不要忽略
r类型判断:r可能是string、error或自定义 struct,直接打印可能不友好 - 若使用结构化日志(如 zap),应把
r作为error字段传入,堆栈作为stack字段单独记录
如何统一管理大量 goroutine 的 panic 捕获
手动在每个 go func() 里写 defer recover 易遗漏、难维护。推荐封装一个带 recover 的启动函数:
func GoWithRecover(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in anonymous goroutine: %+v\n%s",
r, debug.Stack())
}
}()
f()
}()
}使用时:GoWithRecover(func() { ... })
- 不要试图用全局 panic handler 替代 per-goroutine recover —— Go 没有等价机制
- 若 goroutine 有明确生命周期(如 worker),应在 worker 函数入口统一 defer recover,而非每个子操作都套一层
- 注意:该封装无法捕获
os.Exit()或 runtime.Crash 等非 panic 终止
recover 后是否该重新 panic 或传播错误
recover 的目的不是“吞掉” panic,而是防止进程级崩溃,并争取做清理或上报。是否继续 panic 取决于场景:
- 若 panic 表明程序状态已不可信(如内存越界、map 写入 nil),不应恢复,而应让其 crash 并靠监控告警
- 若属于可预期的业务异常(如第三方 API 返回非 JSON),recover 后应转为 error 返回或发送到错误 channel,而不是静默忽略
- 不要在 recover 后调用
panic(r)向上抛 —— 这不会到达调用方,只会再次崩溃当前 goroutine - 若需通知上游,用 channel 或 callback,例如:
errCh
真正容易被忽略的是:recover 后 goroutine 就结束了,但它的资源(如打开的文件、数据库连接)未必被释放 —— 所以 defer 里的 cleanup 逻辑必须独立于 recover 是否发生。


















