必须在e.Use(middleware.Recover())之后注册路由,否则panic导致连接挂起、无响应;该中间件仅捕获主goroutine panic,需为异步goroutine手动recover;默认只打日志不返回JSON错误,须自定义封装;生产禁用e.Debug=true,日志必带traceID和完整堆栈。

必须在e.Use(middleware.Recover())之后才注册路由
没加这句,panic 一发生,HTTP 连接就挂起,前端收不到任何响应,日志里可能只剩一行 stack trace。这不是偶发问题,是上线即高危配置缺失。middleware.Recover() 是 Echo 唯一能捕获 runtime panic 的机制,e.HTTPErrorHandler 完全不接管 panic,只处理框架层错误(比如 c.JSON() 序列化失败)。
常见错误写法:e := echo.New(); e.GET("/ping", handler); e.Use(middleware.Recover()) —— 路由已注册,中间件后加,等于没加。正确顺序必须是:
e := echo.New()-
e.Use(middleware.Recover())← 必须紧随New()后 -
e.GET(...)等路由注册放在这之后
middleware.Recover() 默认只打日志,不返回结构化错误
它默认行为只是调用 log.Printf 打印 panic 堆栈,然后返回空响应体 + HTTP 500。前端拿到的是无意义的空白或默认错误页,无法做错误分类或重试判断。
要返回统一 JSON 错误格式,得自己封装一层:
- 用
middleware.RecoverWithWriter替代原生Recover,传入自定义io.Writer拦截日志 - 或直接写自定义中间件:在
defer func()里调用debug.Stack(),再用c.AbortWithStatusJSON(500, map[string]string{"error": "server error"}) - 务必在
recover()后立即调用c.Abort(),否则后续中间件(比如鉴权、日志)仍会执行
goroutine 内 panic 不会被 middleware.Recover() 捕获
这个中间件只作用于当前 HTTP 请求 goroutine。如果你在 handler 里启动了新 goroutine(比如异步发消息、写日志、调第三方 API),那个 goroutine 里的 panic 完全不会被兜住——主请求照常返回,后台 goroutine 却静默崩溃,还可能泄漏资源。
必须手动为每个独立 goroutine 加 recover:
go func() { defer func() { if r := recover(); r != nil { log.Printf("async panic: %v", r) } }(); doAsyncWork() }()- 别把
recover()写在普通代码块里(恒返回nil),也别在panic()之后才注册defer - 如果用了
errgroup.Group,每个子任务都得自己 recover,eg.Wait()只返回第一个 error,不解决 panic 泄漏
生产环境要关掉 e.Debug = true,但日志必须带 traceID
e.Debug = true 会在 panic 时返回完整堆栈到响应体,开发期方便,但生产环境绝对禁止——会暴露内部路径、变量名、依赖版本等敏感信息。
真正关键的是日志上下文:
- 确保
server.ErrorLog配置到位,否则 panic 日志可能被丢弃(尤其用了自定义http.Server时) - 在 recover 中记录日志时,必须带上请求级 traceID(比如从
c.Request().Context()提取),否则排查时根本串不起链路 - 别只记
err.Error(),要用runtime/debug.Stack()拿完整调用链,且避免在 recover 后继续使用已损坏状态(如已 close 的 channel)
e.Use(middleware.Recover()),而是确认所有 goroutine 都被覆盖、所有错误路径都有 traceID、所有 recover 后都不带病运行。漏掉任意一个,线上就可能变成“看起来正常,实则悄悄丢数据”。


















