3. 错误码设计原则:HTTP 状态码类比、业务错误码分类、错误码与错误消息分离

错误码设计是构建可观测、可诊断、可维护的 Go 服务的基础。本章从三个核心维度展开:HTTP 状态码类比帮助理解分层语义,业务错误码分类提供可落地的编码体系,错误码与错误消息分离确保接口的稳定性和国际化能力。

3.1 HTTP 状态码类比:从网络层到业务层

HTTP 状态码的分层设计(1xx/2xx/3xx/4xx/5xx)是业界公认的最佳实践。业务错误码可以借鉴其前缀分类 + 数字编号的结构,但需要针对业务场景做适配。

HTTP 状态码范围 语义 业务错误码类比 Go 示例场景
2xx (成功) 请求被正常处理 0 / 200000 (成功) 查询用户信息成功
4xx (客户端错误) 请求参数、权限、资源不存在 400xxx ~ 403xxx 参数校验失败、Token 过期
5xx (服务端错误) 系统内部故障、依赖超时 500xxx ~ 503xxx 数据库连接池耗尽、下游 RPC 超时
1xx (信息) 协议升级、中间状态 100xxx (重定向/重试) 请求需要重定向到新节点
关键原则:业务错误码不应直接复用 HTTP 状态码的数字,而是采用独立编码体系,仅在语义上对齐。例如 HTTP 400 对应业务错误码 400001(参数缺失),而非直接返回 400。

3.2 业务错误码分类:三段式编码结构

推荐使用 6 位数字 的三段式结构:ABC DEF,其中 A 为错误等级,BC 为模块标识,DEF 为具体错误编号。

// 错误码结构:A BC DEF
// A:    错误等级 (1=系统级, 2=服务级, 3=业务级, 4=参数级)
// BC:   模块标识 (01=用户, 02=订单, 03=支付, 04=搜索)
// DEF:  具体错误编号 (001~999)

const (
    // 系统级 - 通用
    ErrSystemInternal   = 100001 // 系统内部错误
    ErrSystemTimeout    = 100002 // 系统超时

    // 服务级 - 用户模块
    ErrUserNotFound     = 201001 // 用户不存在
    ErrUserAuthFailed   = 201002 // 认证失败

    // 业务级 - 订单模块
    ErrOrderNotExist    = 302001 // 订单不存在
    ErrOrderStatusLock  = 302002 // 订单状态锁定

    // 参数级 - 搜索模块
    ErrSearchParamEmpty = 403001 // 搜索关键词为空
    ErrSearchParamLen   = 403002 // 搜索词过长
)
编码建议:错误码的模块标识(BC 段)应保持与微服务名称或目录结构一致,便于通过错误码快速定位所属服务。例如搜索服务所有错误码以 04 开头。

3.3 错误码与错误消息分离

错误码是机器可读的稳定标识,错误消息是人类可读的动态描述。两者必须分离,理由如下:

  • 国际化支持:同一错误码可对应中、英、日等多语言消息,无需修改代码。
  • 接口稳定性:错误码一旦发布即不可变更,错误消息可随时优化措辞。
  • 日志聚合:按错误码聚合告警比按文本匹配更可靠。
  • 客户端兼容:客户端仅依赖错误码做逻辑判断,不解析消息文本。
// 错误码与消息分离示例
type ErrorCode int

func (e ErrorCode) Message(lang string) string {
    m := map[ErrorCode]map[string]string{
        ErrUserNotFound: {
            "zh": "用户不存在",
            "en": "user not found",
            "ja": "ユーザーが見つかりません",
        },
        ErrOrderNotExist: {
            "zh": "订单不存在",
            "en": "order does not exist",
        },
    }
    if msg, ok := m[e][lang]; ok {
        return msg
    }
    return m[e]["en"] // 默认英文
}
常见反模式:将错误消息硬编码在错误码定义中,例如 ErrUserNotFound = errors.New("用户不存在")。这会导致无法切换语言、无法统一管理、无法在日志中按错误码聚合。

3.4 错误码设计全景图

下图展示了从错误码定义到最终响应的完整链路,强调分层隔离与消息分离的核心设计。

错误码设计全景图:分层定义 → 消息分离 → 响应输出 错误码定义层 ErrUserNotFound = 201001 错误码定义层 ErrOrderNotExist = 302001 错误码定义层 ErrSearchParamEmpty = 403001 错误消息映射层(多语言 / 动态参数) 201001 → {"zh":"用户不存在", "en":"user not found"} 302001 → {"zh":"订单不存在", "en":"order does not exist"} 响应输出层(JSON / gRPC / 日志) {"code":201001, "message":"用户不存在", "trace_id":"abc123"} 日志: [ERROR] code=201001 msg=用户不存在 trace=abc123 核心原则:错误码不变,消息可换;客户端只认 code,不解析 message

3.5 错误码分类速查表

以下表格汇总了常见业务场景下的错误码分类建议,可直接作为团队编码规范参考。

错误等级 (A) 模块 (BC) 错误码示例 说明
1 (系统级) 00 (通用) 100001 系统内部错误,需立即告警
1 (系统级) 00 (通用) 100002 系统超时,需检查依赖
2 (服务级) 01 (用户) 201001 用户不存在
2 (服务级) 01 (用户) 201002 认证失败
3 (业务级) 02 (订单) 302001 订单不存在
3 (业务级) 02 (订单) 302002 订单状态锁定
4 (参数级) 03 (搜索) 403001 搜索关键词为空
4 (参数级) 03 (搜索) 403002 搜索词过长
实战建议:在 Go 项目中,将错误码定义放在独立的 pkg/errcode 包中,消息映射放在 pkg/errcode/message 子包。错误码使用 const + iota 或手动编号,确保发布后永不修改已存在的错误码值。