recover 必须放在每个 goroutine 内部的 defer 中才能捕获其 panic,因为 go 的 recover 仅对当前 goroutine 有效;外层函数的 defer 无法捕获子 goroutine 的 panic,导致循环任务中断。

recover 必须包在每个独立 goroutine 的 defer 里,否则一次 panic 就会让整个循环任务永久中断。
为什么 recover 放在主函数 defer 里对循环任务无效
常见错误是把 recover 写在启动 goroutine 的外层函数里,比如:
func startWorker() {
defer func() {
if r := recover(); r != nil {
log.Println("this will NOT catch worker panic")
}
}()
go func() {
for range jobs {
doWork() // 这里 panic → 整个 goroutine 崩溃,但外层 defer 不执行
}
}()
}
Go 的 recover 只对**当前 goroutine** 中触发的 panic 有效。主函数的 defer 和子 goroutine 是两个独立调度单元,彼此栈不连通。
- goroutine A panic → 只能被 A 内部的 defer + recover 捕获
- goroutine B panic → A 的 defer 完全无感知,进程不会崩溃,但 B 就此静默退出
- 循环任务一旦所在 goroutine panic 退出,
for就永远停了,后续任务全部丢失
正确写法:每个任务 goroutine 自带 recover 包裹
必须把 defer + recover 放进实际执行逻辑的 goroutine 内部,且位置要包裹住整个循环体:
go func() {
defer func() {
if r := recover(); r != nil {
log.Error("task loop panicked:", r)
// 可选:上报监控、记录失败指标、触发告警
}
}()
for job := range jobChan {
doWork(job) // 即使这里 panic,也会被捕获,循环继续
}
}()
- 只要
doWork内部 panic,recover立即生效,goroutine 不退出,for继续取下一条 - 不要只包单次
doWork调用——那样 panic 后循环就断了;必须包整个for循环 - 如果用
time.Ticker驱动定时任务,同样要把defer+recover放在go func() { ... for { select { case 内部
recover 后要不要重试或跳过当前任务
捕获到 panic 后,程序继续运行,但当前任务状态已不可信。是否重试取决于业务语义:
- 若任务幂等(如发通知、更新缓存),可直接
continue,下次 tick 或下条消息再试 - 若任务不可重入(如扣库存、写唯一订单),应记录失败并移出队列,避免重复消费
- 别在
recover块里调用可能再次 panic 的函数(比如未加锁的 map 写操作) - 日志中务必打印
runtime/debug.Stack(),否则只有 panic message,无法定位哪行代码崩的
容易被忽略的边界点
线上最常翻车的不是没写 recover,而是 recover 写对了但没处理好副作用:
-
recover成功后,原 panic 的栈已销毁,但 goroutine 仍在运行——如果任务里持有锁、打开文件、占着 channel 发送端,这些资源不会自动释放 - 用
defer注册的清理函数(如file.Close())仍会执行,但前提是它们在 panic 前已被defer注册;动态生成的 defer(比如在循环内)可能根本没注册上 - 如果任务逻辑依赖某个初始化成功的全局变量(比如 DB 连接池),而 panic 是因连接池已关闭导致,recover 后不检查状态就继续跑,大概率二次 panic
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











