recover不能直接用于消息消费异常的重试或跳过,仅能防止goroutine崩溃,且必须配合defer在同一goroutine内使用;常见失效原因是defer未在panic发生前、同一函数作用域注册,或SDK自动启新goroutine导致外层defer不生效。

recover 不能直接用于消息消费异常的“重试”或“跳过”,它只负责防止 goroutine 崩溃,且必须配合 defer 在同一 goroutine 内使用——这是绝大多数消息处理代码出问题的第一关。
为什么 consumer goroutine 里加了 defer+recover 却没生效
常见现象:在 go func() { defer recover(); consume(msg) }() 里加了 recover,但消息处理 panic 后日志没打、程序没捕获、甚至整个服务卡住。
根本原因不是写法错,而是执行时机错:recover() 必须出现在 panic 发生前、且在同一函数作用域注册的 defer 中。如果 consume(msg) 是个独立函数,而 defer 写在它的外层匿名函数里,那没问题;但如果误把 defer 放在 consume 函数内部、又没覆盖所有 panic 路径(比如分支里漏了 defer),就会失效。
更隐蔽的问题是:有些消息 SDK(如 sarama、nats.go)默认在回调中起新 goroutine 执行 handler,你在外层注册的 defer 完全不生效。
立即学习“go语言免费学习笔记(深入)”;
- 检查你用的 client 是否自动封装 goroutine —— 查文档或源码里有没有
go fn()这类调用 - 确认
defer func() { recover() }()确实写在最终执行业务逻辑的那个函数最开头 - 避免在
consume函数里用if err != nil { return }就结束,却忘了 panic 可能发生在 return 之后的 defer 执行期间
如何正确包裹单条消息处理逻辑
真正安全的做法,是在每一条消息的处理入口处就完成 defer+recover 注册,而不是依赖外层统一包装。
示例结构:
func handleMsg(msg *Message) {
// 必须放最前面!哪怕后面有解码、校验等前置操作
defer func() {
if r := recover(); r != nil {
log.Error("panic in msg handler", "msg_id", msg.ID, "panic", fmt.Sprint(r))
// 注意:这里不能直接 return,因为函数已 panic,recover 后自动 return
// 后续清理动作(如 ack/nack)需显式做
nackMsg(msg) // 或根据策略选择丢弃、死信、重入队列
}
}()
data := json.Unmarshal(msg.Payload)
result := process(data) // 可能 panic 的业务逻辑
ackMsg(msg)
}
关键点:
-
defer必须在可能 panic 的代码之前注册,顺序不能颠倒 -
recover()返回的是interface{},别直接断言为error,用fmt.Sprint(r)更安全 - recover 后函数立即退出,
ackMsg(msg)不会执行,必须在 recover 块里补上失败路径的处理
多个 goroutine 消费时,每个都要单独加 recover
Go 的 panic 是 goroutine 局部的,主线程或 worker pool 里的 defer 对子 goroutine 无效。你无法靠一个全局 init() 里的 defer 拦住所有消息 handler 的 panic。
典型错误写法:
func main() {
defer func() { recover() }() // 这个永远捕不到 consumer 里的 panic
for range messages {
go func(m *Msg) {
process(m) // 这里 panic,main 的 defer 看不见
}(msg)
}
}
正确做法只有两种:
- 每个
go启动的 handler 内部自己加defer func(){recover()}() - 用封装好的并发工具,比如
errgroup.Group的Go()方法(它内部已做了 recover)
如果你用的是 workerpool 类库,务必确认它是否对每个任务做 recover —— 很多轻量级实现并不做。
recover 后的状态一致性比捕获本身更危险
捕获到 panic 只是第一步,更大的风险在于:你不知道 process() 执行到哪一步就崩了。可能数据库事务已提交一半、文件已写入部分、channel 已发送但未被接收。
这时候强行继续执行后续逻辑(比如发通知、更新状态)大概率导致数据不一致。
建议做法:
- recover 后不做任何业务逻辑,只做三件事:记录 panic、清理当前资源(如关闭临时文件)、明确决定这条消息的命运(ack / nack / send to DLQ)
- 避免在 recover 块里调用依赖外部状态的函数,例如
db.Exec()或http.Post() - 如果必须重试,不要在当前 goroutine 里循环,而是把消息重新投递到队列,由新 goroutine 重新处理
真正难的从来不是怎么写 recover,而是判断 panic 发生后,哪些变量还可信、哪些资源已泄漏、哪条消息该进死信队列——这些没法靠语法自动解决,得靠你对业务流程和资源生命周期的清晰认知。


















