recover 必须在 defer 函数中调用才有效,且 defer 必须在 panic 前注册;它仅捕获同 goroutine 的 panic;传 error 类型值更利于日志与监控;http handler 中应使用 defer+recover 记录堆栈并返回 500。

panic 必须配合 defer 才能被 recover 捕获
recover 只在 defer 函数中调用才有效,其他任何位置调用都返回 nil。这不是设计缺陷,而是 Go 的明确约束:panic 的恢复必须发生在函数退出前的“清理窗口”里。
常见错误是把 recover() 写在普通代码块或 if 语句里,比如:
func badExample() {
panic("oops")
r := recover() // 这行永远不执行,且就算执行了也无效
}
正确写法只有一种模式:
- 必须用
defer包裹一个匿名函数或具名函数 -
recover()必须在这个 deferred 函数内部调用 - defer 必须在 panic 触发之前注册(顺序很重要)
recover 只能捕获同 goroutine 的 panic
Go 的 recover 是 goroutine 局部的,跨协程完全无效。如果你在子 goroutine 里 panic,主 goroutine 的 defer + recover 看不到它,程序照样崩溃。
典型翻车场景:
- 用
go fn()启动一个函数,里面 panic 了 - 主函数写了 defer+recover,但什么也没捕获到
解决办法不是“加强 recover”,而是让每个可能 panic 的 goroutine 自己处理:
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
}
}()
fn()
}()
}
panic 传什么值?优先用 error,别用裸字符串
虽然 panic("xxx") 能跑,但线上出问题时你会后悔——日志里只有模糊文本,没法加字段、没法分类、没法和监控系统对齐。
更稳妥的做法是传 error 类型值:
panic(errors.New("db timeout"))panic(fmt.Errorf("failed to parse %s: %w", input, err))- 或自定义实现了
Error()或String()方法的结构体
这样 recover 拿到的值可以直接参与结构化日志输出,也能被 Sentry、Datadog 等工具自动识别为错误上下文。
HTTP handler 里 recover 的实际写法
Web 服务最需要 recover 的地方就是顶层 handler,否则一次 panic 就让整个请求挂掉,还可能带崩连接池。
标准中间件写法要满足三点:
- defer 必须在 handler 函数开头就注册(不能放在 if 里)
- recover 后要记录完整堆栈(用
debug.Stack()) - 不能吞掉错误——至少要打日志,最好返回 500 并带上 trace ID
示例:
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic at %s: %+v\n%s", r.URL.Path, r, debug.Stack())
http.Error(w, "Internal Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
注意:debug.Stack() 开销不小,生产环境可考虑开关控制,但首次上线建议默认开启。
真正难的不是写对语法,而是判断该不该 panic——大多数业务错误不该 panic,而应该返回 error;只有当程序状态已不可信(比如配置加载失败、关键依赖未初始化)时,panic 才是合理选择。这点容易被忽略,却直接决定系统是“稳住”还是“雪崩”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











