recover不能直接用于重试,它仅捕获panic防止崩溃,但不重执行出错代码;重试需业务层主动发起,且recover后须检查资源状态、避免复用损坏对象,并在新goroutine中启动重试。

recover不能直接用于重试,它只负责捕获panic
很多人误以为 recover 是“自动重试”的开关,其实不是。它只是让程序在 panic 后不崩溃,继续往下跑——但不会帮你重新执行出错的那行代码。你在分布式任务调度中看到的“重试”,本质是业务逻辑层主动发起的第二次调用,recover 只是防止一次失败就把整个 goroutine 或 worker 进程干掉。
典型错误写法:recover() 后直接继续调用 riskyTask(),却不检查状态是否已损坏(比如数据库连接已断、channel 已 close),结果二次 panic。
-
recover返回的是interface{},不是error;别直接传给日志函数或返回给上层,建议用fmt.Sprint(r)格式化 - 必须在
defer函数里调用,且该defer必须注册在可能 panic 的代码之前 - 子 goroutine 的 panic 无法被外层
recover捕获,每个 worker 都得自己加defer func(){ recover() }()
分布式任务调度中 recover 的正确位置:每个 goroutine 入口
比如你用 errgroup.Group 启动一批任务,或用 go task.Run() 处理队列消息,每个独立 goroutine 都要自带防护。主线程里写的 defer+recover 对它们完全无效。
常见结构:
立即学习“go语言免费学习笔记(深入)”;
go func(task Task) {
defer func() {
if r := recover(); r != nil {
log.Error("task panic", "id", task.ID, "err", fmt.Sprint(r))
// 这里可以触发重试逻辑,比如发回队列、写入 retry 表
enqueueForRetry(task)
}
}()
task.Execute() // 可能 panic 的实际工作
}(*task)
- 不要在 handler 或顶层函数里统一 recover——它覆盖不了并发 goroutine
- 如果你用的是
worker pool模式,worker.run()方法开头就要加这个defer - recover 后别复用 task 对象里的资源字段(如
task.DB),先做if task.DB == nil检查,或直接新建依赖
recover + 重试的衔接点:别在 recover 块里直接 retry
真正决定“要不要重试”“重试几次”“等多久”的,是错误类型和上下文,不是 panic 本身。很多 panic 是由不可恢复错误引发的(比如配置缺失、类型断言失败),这种不该重试。
推荐做法:把 recover 当作“兜底熔断器”,只做两件事——记录原始 panic 信息、触发异步重试决策流程。
- 用
errors.Is(err, context.Canceled)或errors.As(err, &net.OpError{})判断是否值得重试,而不是靠recover返回值 - panic 的原因可能是
nil pointer或slice bounds,这类运行时错误说明代码有 bug,应报警而非重试 - 如果
task.Execute()内部明确做了panic(fmt.Errorf("timeout")),那才适合映射为可重试错误 - 重试动作本身必须带
context.Context,防止客户端已放弃,worker 还在傻等
容易被忽略的坑:recover 后的状态污染
这是线上最隐蔽的问题之一:recover 让函数继续执行了,但 panic 发生前可能已经破坏了局部状态。比如:
- 向已关闭的
chan写入导致 panic,recover 后再往同一chan写,必二次 panic - 使用
unsafe或 cgo 调用后 panic,内存可能已泄漏或损坏,继续用原指针极危险 - 事务未 commit/rollback 就 panic,recover 后没手动 rollback,下次操作会卡在锁上
所以,recover 块里唯一安全的操作是:记录、清理、退出。真要重试,就该启动一个全新 goroutine 或新 task 实例,而不是在旧上下文中硬撑。


















