recover必须写在defer函数里,否则返回nil;它仅在panic传播中且goroutine未退出时有效,唯一可靠位置是defer声明的匿名函数内。

recover 必须写在 defer 函数里,否则永远返回 nil
直接调用 recover() 没有意义,它只在 panic 正在传播、且当前 goroutine 尚未退出时起作用。唯一可靠的位置是 defer 声明的匿名函数内部。
常见错误写法:
func bad() {
recover() // 这行永远拿到 nil
panic("boom")
}
正确写法(注意括号嵌套和执行时机):
func good() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()
panic("boom")
}
-
defer确保在函数 return 前执行,此时 panic 还没“烧穿”栈帧 - 多个
defer按后进先出顺序执行,recover()要放在最内层或第一个生效的defer中 - 函数一旦 panic,后续语句(如
fmt.Println("never printed"))不会执行
子 goroutine 的 panic 无法被外层 recover 捕获
HTTP handler、定时任务、异步日志等场景中,panic 往往发生在新起的 goroutine 里,主流程的 defer 完全无效。
立即学习“go语言免费学习笔记(深入)”;
比如这样写就漏掉了:
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in handler: %v", r) // ✅ 捕获主 goroutine panic
}
}()
go func() {
panic("in sub goroutine") // ❌ 这个 panic 永远逃逸
}()
})
- 每个显式启动的
go fn()都必须自带defer func() { recover() }() - 推荐封装
safeGo()工具函数,避免重复写样板代码 - Gin 的
gin.Recovery()、Echo 的middleware.Recover()都只覆盖主请求 goroutine,不处理子协程
recover 后的日志可能不落地,glog.Flush() 是关键
用 glog.Errorf() 记录 panic 后,日志常消失——不是没记,而是没刷盘。glog 默认缓冲写入,panic 导致 goroutine 异常终止,缓冲区来不及 flush。
修正方式很简单,但极易遗漏:
defer func() {
if r := recover(); r != nil {
glog.Errorf("panic in SayHello: %v", r)
glog.Flush() // ✅ 必须加这一行,强制同步写入文件
}
}()
-
glog.Flush()必须在recover()后、函数返回前调用;放错位置(比如 defer 外、或 panic 后才 defer)等于没做 - 若项目已迁移到
klog,对应的是klog.Flush();结构化日志如zap默认同步或可配 sync - 别依赖
log.Fatal()或os.Exit()—— 它们会跳过 defer 执行,Flush()也白搭
recover 返回值不能硬断言,类型安全要兜底
recover() 返回 any,但 panic 可能是字符串、error、自定义 struct,甚至 nil(Go 1.21+ 会转为 *runtime.PanicNilError)。直接 r.(error).Error() 极易二次 panic。
稳妥做法:
- 统一用
fmt.Sprintf("%v", r)转字符串,安全且够用 - 若需 error 接口行为,先
errors.As(r, &e)判断,再操作 - 记录堆栈要用
debug.Stack(),别只打r—— 否则只有 "index out of range",没有哪行代码触发的 - recover 不是重试机制:它不回滚 map 修改、不撤销 channel 发送、不恢复变量状态
真正容易被忽略的,是每个新起 goroutine 的独立防御意识——没人帮你自动加 defer,漏一个,panic 就逃逸一次。


















