业务错误码必须集中定义在 pkg/errcode/errcode.go,禁止在中间件内硬编码 if err == errusernotfound { code = 10001 },否则导致耦合、冲突与维护困难;应通过 apperror 结构体封装 code、msg、httpstatus,由工厂函数预设状态码,中间件仅提取转换,模块前缀需强制约束。

业务错误码必须在中间件外定义、在中间件内消费,不能把码值硬塞进中间件逻辑里。
为什么不能在中间件里写 if err == ErrUserNotFound { code = 10001 }
这种写法会让错误码和中间件强耦合,每次新增一个业务错误都要改中间件——违反开闭原则。更麻烦的是,不同模块(如订单、支付)的错误码可能冲突,中间件无法自动识别来源。
常见错误现象:
• 同一个 10001 在用户模块表示「手机号非法」,在订单模块却变成「地址超长」
• 中间件返回 {"code": 10001, "msg": "user not found"},但前端调用订单接口时也收到这个码,完全懵了
• 单元测试 mock 错误时得同时 mock 中间件行为,用例脆弱易断
- 错误码必须集中定义在
pkg/errcode/errcode.go,每个常量带明确注释 - 每个错误码配套
Text()方法,比如errcode.UserNotFound.Text()返回「用户不存在」 - 中间件只做「提取 + 转换」:从
c.Errors取出AppError,直接读它的Code和Msg字段
c.Errors 怎么承载跨模块的错误码
Gin 的 c.Error() 不是抛异常,而是往上下文里追加一个 *gin.Error 实例。关键在于:你传进去的 Err 字段必须是封装好的 AppError,而不是裸的 errors.New()。
使用场景:
• 用户模块 handler 里调用 c.Error(errcode.NewUnauthorized(errcode.TokenExpired))
• 订单模块 handler 里调用 c.Error(errcode.NewBadRequest(errcode.OrderAmountInvalid))
• 中间件统一取 c.Errors.Last().Err,断言为 *errcode.AppError 后直接取字段
- 工厂函数如
errcode.NewBadRequest()必须返回带HttpStatus和Code的结构体,不能只返回 error 接口 - 避免用
errors.Join()包裹多个AppError——c.Errors是栈式追加,Last()只能取最后一个,多错误需聚合后单次写入 - 如果 handler 主动 return,记得先
c.Error()再 return,否则中间件收不到错误
HTTP 状态码和业务码的映射必须可配置,不能写死
中间件里不能写死 if code == 10001 { status = 404 }。因为同一个业务码在不同网关策略下可能需要不同 HTTP 状态码(比如调试环境返回 200 带错误码,生产环境严格返回 404)。
性能影响:
• 每次请求都查 map 或 switch 会引入微小开销,但比硬编码更可控
• 更大的风险是语义错位:500 状态码本该表示服务不可用,结果被用来返回「参数错误」,触发监控误告
- 推荐做法:在
AppError结构体里保留HttpStatus int字段,由工厂函数按错误类型预设(如NewNotFound()设为 404) - 中间件直接取
appErr.HttpStatus,不二次判断 - 如果真要动态映射,用全局变量或配置项加载的
map[int]int,避免每次新建
最容易被忽略的一点:错误码的模块前缀(如用户模块用 10xxx,订单用 20xxx)必须在定义阶段就强制约束,不能靠开发自觉。建议用 go:generate 自动生成校验逻辑,或者在 CI 阶段用 ast 解析检查常量命名是否符合 ModuleXxx 规则。











