默认gin.recovery()破坏api一致性,因它捕获panic后返回无code字段、无request_id的html页面,且与自定义错误中间件共存易触发多次writeheader panic;必须禁用并改用customrecovery(),配合c.abortwithstatusjson()输出结构化json,并确保错误码、http状态码、traceid、日志全链路对齐。

Gin 微服务里统一错误处理不是加个中间件就完事,关键得让 panic、业务错误、HTTP 状态码、错误码(如 1001)、日志追踪全部对齐,否则前端收不到结构化 JSON,运维查不到 trace ID,开发还在 c.JSON(400, ...) 里硬编码。
为什么默认 gin.Recovery() 会破坏 API 一致性
它捕获 panic 后直接返回 HTML 页面,不是 JSON;而且不带 code 字段、不透传 request_id,和你定义的业务错误格式完全割裂。更麻烦的是,它和自定义错误中间件共存时容易触发 http: multiple response.WriteHeader calls panic——因为两者都试图写响应头。
- 必须禁用默认
gin.Recovery(),改用自定义CustomRecovery() -
CustomRecovery()里defer中的return不可省,否则c.Next()继续执行,可能二次写响应 - 不要在
CustomRecovery()里调用c.Error(),它只存错不输出,这里该直接c.AbortWithStatusJSON() - 日志记录要用
log.Printf("PANIC: %+v", err),而不是fmt.Printf,否则丢失堆栈
ctx.Error() 和 ctx.Errors 的真实作用
c.Error(err) 只是把错误推入 c.Errors 队列,不中断流程也不输出任何东西;c.AbortWithError() 也一样,只是加错 + c.Abort()。它们全是“信号”,不是“响应”。真正干活的是后续中间件读取 c.Errors 并统一渲染。
基于三引擎设计,从微信文章、新闻和博客网页提取干净内容,支持标题作者日期元数据,多格式和批量处理。
- 检查错误必须用
len(c.Errors) > 0,不能靠err != nil判断 - 错误类型要能提取 HTTP 状态码,比如实现
interface{ Status() int },否则 fallback 到500 - 别直接用
c.Errors.Last().Error()返回给前端,敏感信息(如文件路径、SQL 片段)会泄漏 - 业务错误建议用
&AppError{Code: 1001, Msg: "用户名不能为空", TraceID: c.GetString("trace_id")}封装,而非errors.New()
如何让错误码 1001 和 HTTP 状态码 400 分开语义又协同工作
HTTP 状态码用于网关/反向代理做重试或熔断(比如 5xx 触发告警),业务错误码 1001 用于前端展示具体原因或埋点统计。两者必须解耦,但又得在同一个 JSON 响应里共存。
- 响应结构体必须同时含
code(业务码)和status(HTTP 状态码)字段,例如:{"code":1001,"message":"参数校验失败","status":400,"request_id":"abc123"} - 定义错误码规则:前两位表示模块(
10用户模块),后三位表示具体错误(001用户名为空),避免和 HTTP 状态码混淆 - 中间件中优先从错误实例取
Status(),再 fallback 到映射表(如map[int]int{1001: 400, 5001: 500}) - 数据库连接失败这种系统级错误,
code用5001,status用500;参数错误用1001+400,语义清晰且可被监控系统识别
跨服务调用时错误链怎么不断掉
微服务之间用 HTTP 或 gRPC 调用,原始错误一旦序列化成 JSON 再反序列化,堆栈和 cause 就丢了。如果下游返回 {"code":5001,"message":"DB timeout"},上游再包一层,trace ID 和根因就不可追溯。
- 客户端封装层必须解析响应 JSON,重建
*AppError,并用fmt.Errorf("call user service failed: %w", origErr)包裹 - 所有中间件和客户端都要读取并透传
X-Request-ID或trace_idheader,写入错误结构的TraceID字段 - 日志打点必须包含
trace_id和error_code,例如zap.Error(err), zap.String("trace_id", c.GetString("trace_id")), zap.Int("error_code", appErr.Code) - 别在错误消息里拼接下游返回的原始
message,防止嵌套污染,应统一用上游语义重写,如“用户服务不可用”而非“dial tcp: i/o timeout”
最常被忽略的一点:错误中间件注册顺序必须在所有路由 router.GET 之前,且 CustomRecovery() 要放在日志中间件之后——否则 panic 发生时日志还没打全,trace_id 为空,查问题直接抓瞎。










