4. 错误码分类设计:HTTP状态码映射、业务错误码范围划分、预留区间策略
错误码分类是错误码断言体系的基础骨架。一个设计良好的分类方案,能让开发者在看到错误码的瞬间就判断出:错误发生在哪一层、严重程度如何、应该由谁处理。本章从三个维度展开:HTTP 状态码映射(对外协议对齐)、业务错误码范围划分(内部领域隔离)、预留区间策略(未来扩展保障)。
4.1 三层错误码模型
在 Go 微服务架构中,错误码通常分为三层,每层承担不同的职责:
| 层级 | 名称 | 示例 | 使用者 |
|---|---|---|---|
| L1 | HTTP 状态码 | 400, 401, 403, 404, 500 | 网关、客户端 |
| L2 | 业务错误码 | 10001, 20003, 30010 | 服务间调用、SDK |
| L3 | 内部断言码 | ASSERT_ORDER_LOCK_FAILED | 开发、运维、测试 |
设计原则:L1 面向外部协议,L2 面向业务逻辑,L3 面向内部诊断。三层之间通过映射表关联,但各自独立演进。
4.2 HTTP 状态码映射策略
HTTP 状态码是错误码体系对外的“第一张脸”。映射策略需要平衡语义准确性和客户端兼容性:
- 4xx 系列:客户端错误。包括参数校验失败(400)、认证失败(401)、权限不足(403)、资源不存在(404)、请求冲突(409)、频率限制(429)。
- 5xx 系列:服务端错误。包括内部错误(500)、服务不可用(503)、网关超时(504)。
- 2xx 系列:成功。业务层即使有逻辑错误(如余额不足),只要请求本身合法,仍返回 200,错误信息放在业务错误码中。
常见误区:不要将业务逻辑错误(如“订单已取消”)映射为 500。500 只应表示服务端无法处理请求,而非业务规则拒绝。业务拒绝应使用 200 + 业务错误码,或 409/422 等精确状态码。
推荐映射表(Go 项目通用版):
| HTTP 状态码 | 含义 | 典型业务场景 | 断言处理方式 |
|---|---|---|---|
| 200 | OK | 请求成功,业务正常 | 正常返回 |
| 400 | Bad Request | 参数格式错误、必填字段缺失 | 断言失败,返回客户端提示 |
| 401 | Unauthorized | Token 过期、签名错误 | 断言失败,触发重新登录 |
| 403 | Forbidden | 无操作权限 | 断言失败,记录越权日志 |
| 404 | Not Found | 资源 ID 不存在 | 断言失败,返回空或降级 |
| 409 | Conflict | 数据版本冲突、重复创建 | 断言失败,触发重试或人工介入 |
| 422 | Unprocessable Entity | 业务规则校验失败(如余额不足) | 断言失败,返回业务提示 |
| 429 | Too Many Requests | 接口限流 | 断言失败,等待后重试 |
| 500 | Internal Server Error | 数据库异常、panic 未恢复 | 断言失败,触发告警 |
| 503 | Service Unavailable | 依赖服务宕机、熔断开启 | 断言失败,触发降级 |
4.3 业务错误码范围划分
业务错误码是 L2 层的核心,通常使用 6 位或 8 位数字,按模块、功能、错误类型分段。以下是一个经过多个生产项目验证的划分方案:
| 错误码范围 | 所属模块 | 说明 |
|---|---|---|
| 100000 - 199999 | 用户中心 | 注册、登录、鉴权、用户信息 |
| 200000 - 299999 | 订单服务 | 创建订单、支付、退款、取消 |
| 300000 - 399999 | 商品服务 | 库存、价格、分类、搜索 |
| 400000 - 499999 | 支付服务 | 渠道对接、对账、退款 |
| 500000 - 599999 | 通知服务 | 短信、邮件、推送 |
| 600000 - 699999 | 基础组件 | 缓存、数据库、消息队列 |
| 700000 - 799999 | 预留扩展区 | 未来新增模块 |
| 800000 - 899999 | 内部断言专用 | 开发期断言、测试桩 |
| 900000 - 999999 | 全局公共错误 | 参数校验、限流、熔断 |
划分要点:每个模块预留 10 万位空间,前两位标识模块,后四位标识具体错误。例如 100001 表示“用户不存在”,100002 表示“密码错误”。模块间不重叠,便于日志聚合和告警分组。
4.4 预留区间策略
预留区间是错误码体系长期可维护性的关键。很多项目在初期只定义了当前用到的错误码,半年后新模块加入时发现码段已乱,不得不重构。以下是三种经过验证的预留策略:
- 模块级预留:每个模块分配一个连续区间(如 100000-199999),即使当前只用了前 10 个码,剩余空间也不允许其他模块占用。
- 版本级预留:在错误码中嵌入版本位(如 1 位版本号 + 5 位业务码),支持未来版本扩展而不破坏现有映射。
- 断言级预留:专门划出一个区间(如 800000-899999)给内部断言使用,这些错误码不会出现在生产环境的对外接口中,仅用于单元测试和集成测试的断言验证。
避坑提示:不要使用“顺序递增”方式分配错误码。例如用户模块从 10001 开始,订单模块从 10002 开始,这种方案在模块数量超过 10 个后必然冲突。务必使用“区间跳跃”分配法,每个模块至少预留 1000 个码位。
4.5 错误码分类架构图
下图展示了三层错误码模型与 HTTP 状态码映射、业务范围划分、预留区间之间的整体关系:
4.6 错误码断言中的分类实践
在 Go 代码中,错误码分类直接影响断言函数的实现。以下是一个基于分类的断言示例:
// 定义错误码分类常量
const (
// 模块区间
ErrCodeUserNotFound = 100001
ErrCodeUserPasswordWrong = 100002
ErrCodeOrderCreateFailed = 200001
ErrCodeOrderNotPaid = 200002
// 预留区间(内部断言专用)
ErrCodeAssertTestOnly = 800001
)
// 断言函数:根据错误码分类执行不同逻辑
func AssertErrorCode(err error, expectedCode int) bool {
if err == nil {
return false
}
// 提取业务错误码
bizCode := ExtractBizCode(err)
// 分类判断
switch {
case bizCode >= 100000 && bizCode < 200000:
// 用户模块错误:记录审计日志
log.Printf("[UserModule] assertion failed: code=%d", bizCode)
case bizCode >= 200000 && bizCode < 300000:
// 订单模块错误:触发订单补偿流程
triggerOrderCompensation(bizCode)
case bizCode >= 800000 && bizCode < 900000:
// 内部断言错误:仅测试环境生效
if !isTestEnv() {
return false
}
}
return bizCode == expectedCode
}
实战建议:在 Go 项目中,将错误码分类定义放在独立的
internal/errcode 包中,所有模块引用同一个分类表。不要在各自模块中独立定义错误码,否则会导致码段冲突和重复。
4.7 分类设计的常见反模式
以下是实际项目中容易出现的错误码分类问题,以及对应的改进方案:
| 反模式 | 问题描述 | 改进方案 |
|---|---|---|
| 扁平化枚举 | 所有错误码放在一个枚举中,无模块区分 | 按模块划分区间,每个模块独立文件 |
| HTTP 状态码与业务码混用 | 将 404 直接作为业务错误码返回 | L1 和 L2 分离,通过映射表关联 |
| 无预留区间 | 新模块加入时只能插入已有区间 | 提前分配 700000-799999 作为扩展区 |
| 错误码重复使用 | 不同模块使用了相同的数字码 | 建立全局注册表,CI 阶段校验唯一性 |
| 断言码与生产码共用 | 测试断言错误码出现在线上日志 | 划分独立断言区间,生产环境过滤 |
4.8 分类设计的演进路线
错误码分类不是一次性设计,而是随着项目规模增长持续演进。推荐以下路线:
- 阶段一(MVP):只划分 3-5 个模块区间,每个区间预留 1000 个码位,不做版本位。
- 阶段二(增长期):模块超过 10 个时,引入区间跳跃分配法,每个模块至少预留 10000 个码位。
- 阶段三(成熟期):引入版本位或服务 ID 位,支持多版本共存和灰度发布。
- 阶段四(平台期):建立自动化错误码注册中心,CI 阶段自动检测冲突和重复。
核心结论:错误码分类设计的本质是“为未来留出空间”。HTTP 状态码映射解决对外协议对齐问题,业务错误码范围划分解决内部领域隔离问题,预留区间策略解决长期扩展问题。三者结合,才能构建一个可维护、可扩展的错误码断言体系。