gin微服务统一错误码的核心是http状态码与业务码解耦,需集中定义五位语义化错误码、通过apperror结构体分离code/msg/traceid,并由中间件动态映射http状态码;禁用默认recovery,自定义errorhandler统一处理c.errors,确保错误可查、可导出、不可硬编码。

Gin 微服务里统一错误码,核心不是写一堆 Success/Fail 函数,而是把 HTTP 状态码和业务状态码彻底解耦,并让错误码定义可查、可导出、不可硬编码。
错误码必须集中定义且带语义,不能散落在 handler 里
常见错误现象:不同 handler 返回 1001、ERR_USER_NOT_FOUND、"user not exist" 三种写法,前端无法做 switch 判断,日志也搜不到统一标识。
实操建议:
- 新建
pkg/errcode/errcode.go,所有业务错误码用const定义,注释说明场景,例如:const UserNotFound = 10001 // 用户模块-查询失败-用户不存在 - 配套实现
Text(code int) string方法,避免字符串硬编码;调用errcode.Text(errcode.UserNotFound)拿提示语 - 禁止在 handler 里直接写
c.JSON(404, gin.H{"code": 10001})—— 必须通过工厂函数生成,如errcode.NewNotFound(errcode.UserNotFound) - 五位数字分层编码更易维护:前两位表示模块(10=用户,20=订单),后三位表示错误类型(001=未找到,002=参数非法)
AppError 结构体必须分离 HTTPStatus 和业务 Code
响应体里混着 status: 404 和 code: 10001 不是风格问题,是契约问题。网关靠 status 做重试,前端靠 code 做 toast 或跳转,两者语义不同、用途不同、变更频率也不同。
实操建议:
-
AppError字段只保留Code int(业务码)、Msg string(用户提示)、TraceID string(链路追踪),不存 HTTPStatus - HTTP 状态码由中间件根据错误类型动态映射,例如
errcode.UserNotFound → http.StatusNotFound,映射关系走map[int]int静态表,初始化加载一次 - 未显式映射的
Code默认 fallback 到http.StatusInternalServerError,避免返回 200 + 错误体这种反直觉响应 - 响应结构体 JSON key 统一为
code、msg、data,不要出现message、error、result等变体
CustomRecovery 必须替换默认 gin.Recovery(),且注册顺序最前
默认 gin.Recovery() 返回 HTML 页面、不带 code 字段、不透传 trace_id,和你定义的业务错误格式完全割裂。更麻烦的是,它和自定义错误中间件共存时容易触发 http: multiple response.WriteHeader calls panic。
实操建议:
- 禁用默认 recovery:
router.Use(CustomRecovery()),且必须放在所有其他中间件之前 -
CustomRecovery()里 defer 后必须return,否则c.Next()继续执行,可能二次写响应头 - panic 捕获后直接调用
c.AbortWithStatusJSON(http.StatusInternalServerError, response.Error(5000, "系统异常")),别用c.Error()—— 它只是存错,不输出 - 日志记录必须用
log.Printf("PANIC: %+v", err)或logger.Errorw("PANIC", "err", err),不用fmt.Printf,否则丢堆栈
ErrorHandler 中间件必须检查 c.Errors 而非 err != nil
c.Error() 是信号机制,不是响应动作。它把错误推入 c.Errors 队列,但不会中断流程、也不会自动渲染 —— 你得在请求生命周期末尾主动读取并转换。
实操建议:
- 在
CustomRecovery()之后注册ErrorHandler(),结构为:c.Next(); if len(c.Errors) > 0 { ... } - 判断错误类型必须用
errors.Is(err.Err, ErrInvalidParam),别用err.Error() == "xxx"字符串匹配 - 业务 error 工厂函数(如
ErrInvalidParam)要实现Unwrap() error,支持链式错误追踪 - 确保只在一处出口调用
c.AbortWithStatusJSON(),重复调用会 panic;不要在 handler 里手动c.JSON()处理业务错误
最容易被忽略的一点:错误码的“可导出性”不是指 Go 的首字母大写,而是指它能在文档、Swagger、前端 SDK、监控告警规则里被一致引用。一旦 10001 在代码里是用户未找到,在前端却当成了参数错误,整个链路的信任基础就垮了。











