必须用 middleware.Recover() 拦截 panic,否则请求静默断开;但业务错误需自定义中间件 + c.Error() 链式传递并统一由 HTTPErrorHandler 处理,因 Recover 仅捕获 panic,不处理显式 error 返回。

必须用 middleware.Recover() 拦截 panic,否则请求会静默断开;但仅靠它不够——业务错误(如参数校验失败、权限拒绝)得靠自定义中间件 + c.Error() 链式传递,再统一收口到 HTTPErrorHandler。
为什么默认 Recover 中间件不能处理所有错误
它只捕获 panic,不处理 return errors.New("xxx") 这类显式错误。Echo 的设计哲学是“错误即值”,panic 是意外崩溃,业务错误应主动返回。若只依赖 Recover,你将发现:400/401/403 等状态码无法触发、前端收不到结构化 code 字段、日志里查不到参数校验失败的上下文。
常见错误现象:panic("user not found") 被转成 500 响应,但 c.Error(errors.New("invalid id")) 完全没反应,请求直接返回空 200 或超时。
-
Recover必须是e.Use()注册的第一个中间件,否则前置中间件(如 JWT 验证)里的 panic 逃逸出去 - 它不调用
e.HTTPErrorHandler,所以你在HTTPErrorHandler里写的日志、监控、响应格式逻辑对 panic 无效 - 子 goroutine 中的 panic 不会被捕获,必须在每个
go func()内手动加defer recover()
HTTPErrorHandler 是统一响应的真正入口
它负责把所有可识别的错误(*echo.HTTPError、error、甚至 panic 包装后的 echo.HTTPError)转成标准 JSON。但注意:它不会自动触发——必须由你主动调用 c.Error() 或框架内部抛出 *echo.HTTPError 才进入这个流程。
典型使用场景:参数绑定失败、权限校验不通过、数据库查询为空等非崩溃型错误。
- 在 handler 里别直接
return c.JSON(400, ...),改用return c.Error(&echo.HTTPError{Code: 400, Message: "invalid param"}),这样才会进HTTPErrorHandler -
HTTPErrorHandler函数内必须手动设置c.Response().Status和写响应体,Echo 不帮你自动渲染 - 生产环境建议对 5xx 错误隐藏原始
Message,只保留code和泛化提示,避免泄露敏感路径或 SQL 片段
如何设计分层错误码与响应结构
HTTP 状态码(如 400/401/403/500)和业务错误码(如 1001 表示“用户不存在”)要分开。前者控制客户端重试/跳转行为,后者供前端展示具体提示或触发特定 UI 动作。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
推荐响应结构:
{"code":1001,"message":"用户不存在","data":null}
关键点:
- 定义常量包管理业务码,例如
const ErrUserNotFound = 1001,避免硬编码字符串或数字 - 用
echo.NewHTTPError(404).SetInternal(err)包装底层错误,既保留 HTTP 状态又透出业务码 - 在
HTTPErrorHandler中用类型断言区分:if he, ok := err.(*echo.HTTPError); ok && he.Code == 404→ 返回code: 1001;if errors.Is(err, sql.ErrNoRows)→ 同样映射到1001 - 不要在
HTTPErrorHandler里再 panic,否则会二次触发Recover,造成日志重复、响应体混乱
子 goroutine panic 必须单独 recover
middleware.Recover() 对 go func() { panic("oops") }() 完全无效。这种 panic 会导致当前连接无声关闭、MQ 消息丢失、定时任务中断,且无任何日志。
正确做法是封装一个 safeGo() 工具函数:
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
// 可选:上报 Sentry / Prometheus
}
}()
f()
}()
}
然后全局替换所有裸 go xxx() 为 safeGo(xxx)。漏掉任意一处,就是线上静默故障点。
最易被忽略的是异步写日志、发通知、清理临时文件这些看似“不重要”的子任务——它们 panic 后不会影响主请求响应,但会悄悄腐蚀系统稳定性。

















