http handler 中 recover 必须在函数开头注册,否则 panic 时未注册的 defer 不会执行;需用 error 类型 panic 并在子 goroutine 中单独 recover,避免泄漏和状态不一致。

HTTP handler 里 recover 必须在函数开头注册
panic 发生后,栈会立刻开始回溯,只有在 panic 触发前已注册的 defer 才有机会执行。如果把 defer func() { recover() }() 写在 if 分支里、或放在业务逻辑之后,它根本不会被注册,panic 就直接炸穿整个 goroutine。
常见错误写法:
func badHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {
return
}
defer func() { // ❌ 这里不会执行!/health 路径下 panic 就没 defer
if r := recover(); r != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// ...
}
正确做法:所有 handler 入口第一行就注册 defer —— 不管路径、参数、中间件顺序如何变化,它都在那里。
- 必须是函数体最顶部的语句(不嵌套在任何控制流中)
- 推荐封装成中间件,避免每个 handler 都手写一遍
- 不要依赖框架自动注入;自己显式调用更可控
recover 拿到的值类型决定日志和监控质量
传给 panic() 的值类型,直接影响 recover() 返回值能否被结构化处理。用裸字符串如 panic("db timeout"),recover() 返回的是 string,无法加字段、无法分类、Sentry 也识别不了上下文。
应该统一用 error 类型:
panic(errors.New("config missing"))panic(fmt.Errorf("failed to connect: %w", err))- 自定义 error 结构体,实现
Error() string方法
这样在 recover() 后可以做类型断言,分路径记录、打指标、上报 trace ID:
switch err := r.(type) {
case error:
log.WithField("path", r.URL.Path).WithError(err).Error("http panic")
case string:
log.WithField("path", r.URL.Path).Errorf("http panic (string): %s", err)
}
goroutine 泄漏比 panic 更危险:子 goroutine 必须自带 recover
HTTP handler 里起的子 goroutine(比如异步发消息、清理资源),一旦 panic,主 handler 的 defer+recover 完全看不见——Go 的 recover 是 goroutine 局部的,跨协程无效。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
典型翻车场景:
-
go doSomethingRisky()内部 panic - 主 handler 有 defer+recover,但毫无反应
- 程序没崩溃,但 goroutine 永远卡住、内存持续上涨
解决办法不是“加强主 recover”,而是让每个可能出问题的 goroutine 自己守好门:
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
debug.PrintStack() // 别只打一行日志
}
}()
fn()
}()
}
注意:debug.PrintStack() 要在 defer 函数内调用,否则拿不到 panic 时的完整栈。
recover 不是兜底开关,它掩盖的是设计缺陷
频繁靠 recover 拦住空指针、越界、未初始化 map,说明代码里本该做的防御性检查被跳过了。Go 的哲学是“显式错误优于隐式 panic”,比如:
- map 访问前应先判断是否为 nil,而不是等 panic 后 recover
- channel 操作前检查是否已关闭,而不是靠 recover 捕获 send on closed channel
- 第三方库返回
error时,应处理而非忽略,再用 panic 强行兜底
真正适合 panic/recover 的场景其实很窄:初始化失败、配置校验不通过、断言绝不该发生的逻辑分支。其他情况,优先改代码,而不是加 recover。
最容易被忽略的一点:recover 后的程序状态未必一致。比如 panic 发生在事务中间,recover 拦住了,但数据库已经部分提交——这种“伪恢复”比直接崩溃更难排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










