子协程 panic 无法被主 goroutine 的 recover 捕获,必须在每个子协程内部用 defer + recover 处理,且 recover 后应立即 return 或显式清理资源,不可继续执行原逻辑。
子协程 panic 无法被主 goroutine 的 recover 捕获
这是最根本的限制:go 明确规定 recover 只对**当前 goroutine** 中的 panic 有效。你在 main 或其他函数顶部写的 defer func() { recover() }(),对任何 go func() { ... }() 内部发生的 panic 完全无效。
典型错误现象是:程序仍打印完整 stack trace 并退出,哪怕你“明明加了 recover”。这是因为 panic 发生在子 goroutine,而它的 defer 链里没有 recover,runtime 直接终止整个进程。
- 子 goroutine 是独立调度单元,panic 不传播、不通知、不等待
-
sync.WaitGroup.Wait()或chan等同步机制拦不住未捕获的 panic - 不要幻想“统一兜底”——必须每个可能出错的子协程都自己配
defer+recover
必须在子协程内部写 defer + recover
正确写法是在 go 启动的匿名函数体**最开头**就注册 defer,且 recover 必须在该 defer 函数内调用:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in worker: %v", r)
}
}()
// 这里写可能 panic 的逻辑,比如:
// - 访问未初始化的 map
// - json.Unmarshal 未知结构
// - 反射调用不安全字段
// - 第三方库未校验输入
}()-
recover()必须写在defer函数体内,不能写在普通if判断里,否则永远返回nil - 顺序很重要:
defer要早于可能 panic 的代码执行(靠defer的注册时机保证) - 不要只打一行日志就完事——至少要结合
debug.Stack()或runtime.Stack()获取堆栈
recover 后不能继续执行原业务逻辑
recover 只是让 goroutine 不崩溃,它**不会回滚状态、不会重置变量、不会续上 panic 前那行代码**。常见误用是 recover 后直接往下跑,结果数据错乱或逻辑跳变。
- 推荐做法:记录日志后立即
return,或转入明确错误分支(如关闭资源、上报告警、重试) - 避免在
recover块里再调用可能 panic 的操作(比如向已关闭chan发送、对nil map写入),否则原始 panic 会被覆盖 - 如果需要通知主 goroutine,用
chan error或sync.Once+atomic.Value显式传递,而不是靠“恢复执行”隐式通信
HTTP handler 和守护协程是高频漏点
这两类场景最容易忘记加 recover:HTTP handler 通常由框架启动新 goroutine 处理请求;守护协程(如定时任务、连接保活)长期运行,一旦 panic 就静默退出,导致监控失察、任务中断、连接泄漏。
- HTTP 中间件可封装通用
recovery,但必须确保它作用在每个请求 goroutine 内部(如 Gin 的gin.Recovery()) - 守护协程建议用封装好的
safeGo启动,例如:safeGo(func() { tickerWorker() }, logger) - 封装时别省略上下文:传入
*log.Logger、trace ID、request ID,否则 panic 日志无法关联链路
真正容易被忽略的是 recover 后的资源清理。goroutine 里开了 timer、占了 mutex、持有了 file 句柄,recover 之后若没显式释放,这个 goroutine 就卡住不动,变成泄漏——比 panic 本身更隐蔽。


















