recover必须放在可信边界入口(如HTTP handler、gRPC拦截器、定时任务主循环),而非service或repo层;否则会掩盖panic、遗留脏状态;goroutine需各自recover,且recover后须显式返回或退出。

recover 必须放在可信边界入口,不是每层都加
多层调用链(比如 handler → service → repo → db.Query)里,**不要在 service 或 repo 层写 defer recover()**。那只会让 panic 被局部吞掉,掩盖真实问题,还可能留下脏状态(比如已开启的事务没回滚、连接没释放)。真正该放 recover 的地方,是整个调用链的“可信边界”——也就是你明确知道它会启动新 goroutine、接收外部输入、或承担错误隔离职责的位置。
- HTTP handler 函数开头(或中间件中)
- goroutine 启动时的匿名函数内部
- RPC 方法入口(如 gRPC
UnaryServerInterceptor) - 定时任务主循环(
for range time.Tick里每次迭代前)
这些位置的特点是:它们是控制流进入你代码的“闸口”,且天然具备独立生命周期。在这里统一 recover,既能拦截下游所有层级的 panic,又不会污染业务逻辑。
为什么不能在普通业务函数里 defer recover()
常见翻车现场:func GetUser(id int) (*User, error) 里自己加 defer func(){ recover() }()。这看似“防护周全”,实则三重危害:
- 它捕获了本该由上层处理的 panic(比如空指针解引用),导致错误被静默吞掉,日志里只剩一条“
GetUser returned nil”,根本看不出是 panic 还是正常逻辑返回 - panic 发生后,函数栈已破坏,
GetUser内部的defer可能来不及清理资源(如未关闭的*sql.Rows) - 如果上层 handler 已经有
recover,这里再套一层,等于重复处理,还干扰了错误传播路径
记住:recover 不是错误处理,它是崩溃兜底。业务函数该返回 error 就返回 error,该 panic 就让它 panic —— 让边界层来决定怎么收场。
立即学习“go语言免费学习笔记(深入)”;
诊断并恢复通过 SSH 隧道连接的 OpenClaw 节点。用于解决配对必需错误、隧道冲突、远程端点错误以及 SSH 目标配置错误等问题。
HTTP 中间件里的 recover 必须手动写响应
用 http.HandleFunc("/user", recoverMiddleware(userHandler)) 包装 handler 时,recoverMiddleware 内的 recover() 成功后,w.WriteHeader(500) 和 json.NewEncoder(w).Encode(...) 这两步必须显式写。标准库不帮你补:
- 不写
w.WriteHeader(500)→ 客户端收到 HTTP 200 + 空 body,前端卡死等待 - 直接
fmt.Fprint(w, r)→ 生产环境泄露堆栈、文件路径、变量名 - 没检查
w.Header().Get("Content-Type")→ 如果上游中间件已设为application/json,你再写text/plain会触发http: multiple response.WriteHeader calls
推荐写法:if r := recover(); r != nil { w.WriteHeader(http.StatusInternalServerError); log.Printf("panic in %s: %v", req.URL.Path, r); json.NewEncoder(w).Encode(map[string]string{"code": "500", "message": "Internal Server Error"}) }
goroutine 里的 panic 必须各自 recover
如果 userHandler 里启动了异步 goroutine(比如发邮件、写日志),那个 goroutine 的 panic **绝不会被 handler 的 recover 捕获**。现象是:主请求返回了,但后台 goroutine 崩溃后泄漏连接、卡住 channel、甚至把数据库连接池耗尽。
- 正确做法:每个
go func()开头就写defer func(){ if r := recover(); r != nil { log.Printf("async panic: %v", r) } }() - 别图省事封装成
safeGo(f)就完事——你得确保它在 goroutine 内部执行,而不是在主 goroutine 里注册一个 defer - recover 后别继续用可能已损坏的对象(比如 panic 是因为
map被并发写入,recover 后再读这个 map 会二次 panic)
最易忽略的一点:recover 后的 goroutine 不会自动退出,它只是从 panic 状态软着陆。如果你没在 recover 分支里 return 或 os.Exit,它可能继续跑下去,干出更奇怪的事。

















