recover() 不能捕获子 goroutine 的 panic,仅对当前 goroutine 有效;http handler 中 defer recover 失效常因 panic 发生在 go 启动的子协程中,需用 safego 包装所有显式 goroutine 启动。

recover() 不能捕获子 goroutine 的 panic,也不能替代 error 处理——它只是最后一道防线,用错地方反而掩盖问题。
为什么 HTTP handler 里的 defer recover 经常失效
HTTP server 对每个请求起一个新 goroutine,recover() 只对当前 goroutine 有效。如果 panic 发生在 handler 内部启动的子 goroutine(比如异步日志、定时清理、第三方回调),外层 defer 根本看不到。
- 常见现象:接口返回 500 或直接断连,日志里没 panic 记录,进程却意外退出
- 检查点:翻一遍所有
go fn()调用,尤其是第三方 SDK 的 callback、time.AfterFunc、http.Client.Do后续处理 - 中间件如
gin.Recovery()或echo.Middleware.Recover()仅包裹主请求 goroutine,不延伸到子协程
所有显式 go 调用必须套 safeGo
别指望靠“统一加个 defer”兜底,必须在每个 go 前手动包一层带 recover() 的 wrapper。
- 写一个通用函数:
safeGo(func() { ... }),内部用defer func() { if r := recover(); r != nil { log.Panicf("goroutine panic: %v\n%v", r, debug.Stack()) } }() - 避免裸写
go doSomething(),哪怕只是一行log.Info()—— 第三方 log 库内部可能触发异步 flush - 注意:不要在
safeGo里直接log.Printf("%v", r),它不打堆栈;必须用debug.Stack()或runtime/debug.PrintStack()
recover 必须在 defer 函数里调用,且参数求值时机很关键
recover() 不是普通函数,它只在 panic 正在传播、当前 goroutine 尚未退出时有效。直接调用永远返回 nil。
- 错误写法:
func bad() { recover(); panic("x") }→ 完全无效 - 正确写法:
defer func() { if r := recover(); r != nil { ... } }() - 陷阱:如果 defer 函数有命名返回值,
recover()捕获后函数仍会 return,但不会执行 defer 后的语句(比如fmt.Println("never runs")) - 参数陷阱:写
defer log.Printf("end at %v", time.Now()),时间戳是 defer 声明时的值,不是 panic 发生时;要动态取值得用闭包:defer func() { log.Printf("end at %v", time.Now()) }()
main 函数和初始化阶段也要加 recover
服务启动失败、init 阶段 panic、或 main goroutine 直接 panic,这些都逃不出中间件覆盖范围。
- 在
main()开头加:defer func() { if r := recover(); r != nil { log.Panicf("main panic: %v\n%v", r, debug.Stack()) } }() - 检查所有
init()函数,尤其涉及配置加载、数据库连接池初始化、全局变量赋值的地方 - 别信
debug.SetPanicOnFault(true):它只对非法内存访问(如某些平台的 nil pointer dereference)生效,对panic("xxx")、map 写入、切片越界完全无效
真正难的不是写对 recover(),而是判断该不该用——90% 的业务 panic 其实该转成 error 返回;剩下那 10%,得确保每条 panic 路径都有 traceID、完整 stack trace、以及副作用隔离能力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











