http中间件是recover的唯一合理位置,因为每个http请求在独立goroutine中执行,recover仅对当前goroutine有效,且中间件是所有请求必经的统一入口;main或init中加recover无效,handler内局部recover会漏捕下游panic,必须置于最外层handler前(如servehttp或闭包),并手动写响应头与体、脱敏错误信息、区分环境日志行为,而异步goroutine需各自加recover。

为什么 HTTP 中间件是 recover 的唯一合理位置
因为每个 HTTP 请求都在独立 goroutine 中执行,recover 只对当前 goroutine 有效,而中间件是所有请求必经的统一入口。在 main 函数或 init 里加 defer recover() 完全无效——服务都还没起来,panic 就已经让进程退出了。
常见错误是只在某个 handler 函数里写 defer recover(),结果 panic 发生在它调用的下游函数(比如数据库查询、JSON 解析),就漏捕了。必须把 recover 放在最外层 handler 执行前,也就是中间件的 ServeHTTP 或闭包里。
- 别用
http.HandleFunc直接注册裸函数,一定要显式包装:http.HandleFunc("/user", recoverMiddleware(userHandler)) - 如果用
chi或gorilla/mux,它们自带Recoverer,但默认不检查响应头是否已写入,可能触发http: multiple response.WriteHeader calls - Go-Zero 等框架的
server.Use()是安全的,但需确认中间件注册顺序——必须在业务 handler 之前
recover 后必须手动写响应头和体,否则客户端卡死
recover 只是让 goroutine 不退出,它不会自动回滚已发生的副作用,更不会帮你发 HTTP 响应。标准库在内部 recover 后只打日志,不调用 http.Error,所以前端看到的是空响应或超时。
现象:curl -v 返回 0 bytes,状态码却是 200;浏览器一直 loading;Prometheus 的 http_request_duration_seconds 持续上涨。
- 必须在
recover分支里先调w.WriteHeader(500),否则 Go 默认返回200 OK - 别直接
fmt.Fprint(w, err)—— 生产环境要脱敏,至少过滤掉文件路径和行号,可用正则替换或只输出错误类型 - 如果用了 JSON API,建议统一返回
{"code":500,"message":"Internal Server Error"},避免前端解析失败
开发与生产环境的日志行为必须区分
第三方框架的 gin.Recovery() 或 chi.Middleware.Recoverer 默认把完整堆栈打到 stdout,开发时有用,上线后就是安全隐患——可能泄露源码路径、变量名、甚至配置片段。
真正可控的做法是自己实现中间件,并注入不同行为:
- 开发环境:用
debug.PrintStack()输出完整堆栈(比fmt.Sprintf("%+v", err)更可靠) - 生产环境:只记录
err.Error()+ 请求路径 + 方法 + traceID,堆栈写入单独日志文件或上报 Sentry - Go-Zero 用户可用
middleware.WithRecovery(func(ctx context.Context, err interface{}) error {...})替换默认行为
goroutine 和异步任务里的 panic 不能靠中间件兜底
HTTP 中间件只覆盖请求处理链,对 go func() { ... }()、定时任务、消息队列消费者完全无效。这些 goroutine 一旦 panic,就静默退出,连日志都可能被吞掉。
典型场景:用户注册成功后发邮件,go sendEmail(...) 里 panic,主流程照常返回 200,但邮件永远发不出去。
- 每个 goroutine 入口必须显式加
defer func() { if r := recover(); r != nil { ... } }() - 别在
main里包一层defer recover()—— 对子 goroutine 毫无作用 - 异步任务建议结合
context.Context实现超时与取消,panic 后至少上报监控指标(如 Prometheus Counter)











