recover在goroutine中必须置于其内部最外层defer中并赋值处理,才能捕获本goroutine的panic;仅外部defer或未赋值调用均无效,且recover后不可继续原逻辑,须记录、更新状态、告警或投递死信。

recover 在 goroutine 中根本不起作用
直接在启动 goroutine 的函数里 defer recover(),永远捕获不到 panic。因为每个 goroutine 有独立的调用栈,recover() 只能捕获当前 goroutine 内部的 panic。
常见错误写法:
go func() {
defer recover() // ❌ 无效:recover 没有赋值给变量,且不在 panic 后立即执行
processTask(task)
}()
正确做法是把 recover() 放进 goroutine 内部最外层,并显式处理返回值:
- 必须用
defer func() { if r := recover(); r != nil { /* 处理 */ } }()包裹任务逻辑 - 不能省略
r := recover()赋值,否则 Go 编译器会忽略该调用 - panic 发生在子函数(比如
json.Unmarshal或数据库查询)里,recover 依然有效,前提是 defer 在同一 goroutine 且未返回
任务级 recover 要包裹整个执行单元
队列任务通常由一个结构体或函数表示,比如 type Task struct { ID string; Payload []byte }。recover 必须覆盖从反序列化、业务逻辑到结果回调的完整链路,否则中间某步 panic 仍会导致 goroutine 崩溃。
推荐封装模式:
OpenClaw 基础设施心跳监控与自动恢复:部署/卸载心跳守护进程,配置主机监控(Gateway、磁盘、内存、CPU、进程),设置飞书告警,自动重启故障Gateway。适用关键词:心跳监控、heartbeat daemon、服务器告警、自动重启、健康检查、auto recovery。
func (q *WorkerQueue) runTask(task *Task) {
defer func() {
if r := recover(); r != nil {
log.Printf("task %s panicked: %v", task.ID, r)
q.failTask(task, fmt.Sprintf("panic: %v", r))
}
}()
err := q.decodeAndExecute(task)
if err != nil {
q.failTask(task, err.Error())
}
}
-
decodeAndExecute()里包含json.Unmarshal、数据库操作、HTTP 调用等高风险步骤 - 不要只在业务函数里 recover —— 那样 failTask 本身 panic 就漏掉了
- 如果任务含 callback 函数(如
task.OnSuccess = func(){...}),也要确保 callback 执行也被 defer 包裹
recover 后不能继续执行原逻辑,但可重试或降级
recover 只是“止血”,不是“修复”。一旦 panic 被捕获,程序状态很可能已不一致(例如部分 DB 写入成功、事务未提交、channel 已关闭)。此时继续往下跑大概率引发二次 panic 或数据错乱。
- ✅ 正确做法:记录错误 → 更新任务状态(如设为
failed)→ 触发告警或投递死信队列 - ⚠️ 慎用重试:仅当任务幂等且 panic 是临时性错误(如网络超时)时才考虑
q.retryTask(task, 2) - ❌ 禁止写法:
recover(); log.Warn(...); continue—— continue 在 defer 函数里无意义,且原函数早已 return
注意 recover 对 channel send panic 的局限性
向已关闭的 channel 发送数据会 panic:send on closed channel。这种 panic 可被 recover,但要注意:如果多个 goroutine 并发向同一 channel 写入,仅靠单个 recover 无法避免竞争条件。
- 更健壮的做法是提前检查 channel 状态,或用
select+default避免阻塞写入 - 如果必须 recover,确保 close channel 的逻辑和发送逻辑有明确的生命周期边界(例如 channel 由 worker 生命周期管理,而非任务粒度)
- recover 到
send on closed channel时,通常意味着 worker 已 shutdown,此时应停止处理新任务,而不是重试
真正棘手的是 context 超时后继续操作资源(如往超时的 http.Response.Body 读取),这类问题 recover 不了,得靠 ctx.Done() 检查和资源 cleanup 函数 —— recover 只是最后一道防线,不是替代优雅退出的方案。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










