go中recover仅用于不可控第三方代码、goroutine内部崩溃及明确知悉可能panic的场景,必须与defer绑定于函数入口,记录完整堆栈并结构化日志,不可全局init注册或替代错误检查。

Go里没有“异常恢复模块”这种东西——panic和recover是运行时机制,不是可插拔的模块。想封装成“高效模块”,本质是避免滥用panic,并在真正需要的地方做精准、带上下文的捕获。
什么时候该用 recover 而不是 if err != nil
只在以下情况才值得上recover:
-
panic来自不可控第三方代码(比如调用template.Execute时模板语法错误、json.Unmarshal遇到非法嵌套) - goroutine 内部崩溃不能影响主线程(如 HTTP handler、RPC 方法、后台 worker)
- 你明确知道某段逻辑可能触发运行时 panic(如反射调用、手动
panic),且业务上必须兜底返回而非中断
别在普通业务逻辑里包一层recover来替代错误检查——这掩盖问题,还让调用链失去控制流语义。
recover 必须和 defer 绑定在函数入口
常见错误:把recover写在中间位置,或放在子函数里
正确姿势只有一种:
func handleRequest(w http.ResponseWriter, r *http.Request) {
// ✅ 必须在最外层 defer,且 recover 在 defer 函数体内
defer func() {
if r := recover(); r != nil {
log.Printf("panic in request %s: %v", r.URL.Path, r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// 正常业务逻辑...
}
原因:recover只能捕获当前 goroutine 中、由本函数或其调用链触发的panic;如果 defer 不在顶层,它根本看不到 panic。
recover 后必须记录堆栈,否则等于没做
只打印r(即 panic 参数)毫无调试价值。生产环境至少要:
- 调用
debug.Stack()获取完整调用链 - 附加请求 ID、时间戳、goroutine ID(可用
runtime.GoID(),Go 1.22+) - 避免直接
fmt.Printf,走结构化日志(如log/slog)
示例:
import (
"log"
"runtime/debug"
"slog"
)
defer func() {
if r := recover(); r != nil {
slog.Error("panic recovered",
"panic", r,
"stack", string(debug.Stack()),
"goroutine_id", runtime.GoID(),
)
// ... 返回错误响应
}
}()
不要全局 init 注册 recover
像在 init() 里写 defer recover() 是典型误区——它只对 init 函数本身生效,对后续任何 goroutine 都无效。
真正要覆盖的,是每个可能 panic 的执行入口:
- HTTP handler(中间件里 wrap)
- RPC 方法(服务端 handler 封装)
- goroutine 启动处(
go func() { defer recover() {...} }()) - 定时任务函数(
func() { defer recover() {...} }())
每个入口独立处理,才能保证 panic 不逃逸、不丢失上下文。
最后一点容易被忽略:recover 不是容错,而是止损。它解决不了 panic 的根源——该修 bug 还是要修,该加边界检查还得加。把它当成“最后一道保险”,而不是“可以随便 panic”的许可证。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











