3. 领域错误码设计原则:业务语义优先、可枚举、可扩展、可国际化

领域错误码是领域边界断言失败后的第一道“可读防线”。在 Go 语言中,错误码的设计质量直接决定了调用方能否快速定位问题、运维能否自动化告警、国际化团队能否协同排查。本章围绕四个核心原则展开:业务语义优先、可枚举、可扩展、可国际化。

核心观点:错误码不是给机器看的数字,而是给“人+系统”共同理解的契约。好的错误码设计,能让断言失败时的错误信息具备自解释能力。

3.1 业务语义优先:错误码应直接反映领域行为

传统 HTTP 状态码(如 400、500)只能表达“请求错误”或“服务器错误”,无法区分“订单金额超限”与“用户余额不足”。领域错误码必须携带业务含义。

设计维度 反例(无语义) 正例(业务语义)
错误码 E1001 ORDER_AMOUNT_EXCEEDS_LIMIT
错误消息 invalid request 订单金额 15000 超出单笔限额 10000
断言场景 参数校验失败 领域规则校验失败
Go 实现示例:
// 业务语义错误码定义
type DomainErrorCode string

const (
    ErrOrderAmountExceedsLimit DomainErrorCode = "ORDER_AMOUNT_EXCEEDS_LIMIT"
    ErrUserBalanceInsufficient DomainErrorCode = "USER_BALANCE_INSUFFICIENT"
    ErrProductOutOfStock       DomainErrorCode = "PRODUCT_OUT_OF_STOCK"
)

// 断言函数返回带语义的错误码
func AssertOrderAmount(amount, limit float64) error {
    if amount > limit {
        return &DomainError{
            Code:    ErrOrderAmountExceedsLimit,
            Message: fmt.Sprintf("订单金额 %.2f 超出单笔限额 %.2f", amount, limit),
        }
    }
    return nil
}
业务语义错误码分层结构 HTTP 状态码 400 / 500 / 403 领域错误码 ORDER_AMOUNT_EXCEEDS_LIMIT 错误消息 订单金额 15000 超出限额 业务语义优先原则 错误码直接反映领域行为,而非传输层状态 调用方可根据错误码做精确的业务分支处理 调用方:重试/降级 运维:告警聚合 国际化:消息模板

3.2 可枚举:所有错误码必须在编译期可见

可枚举意味着错误码集合在代码库中是封闭的、可穷举的。Go 语言中推荐使用 const + 自定义类型实现编译期检查,避免运行时字符串拼写错误。

常见陷阱:使用 string 类型直接定义错误码,调用方容易写错字面量(如 "ORDER_AMOUT_EXCEEDS"),且 IDE 无法自动补全。必须使用自定义类型 + const 枚举。
// 推荐做法:自定义类型 + const 枚举
type OrderErrorCode string

const (
    OrderAmountExceeds OrderErrorCode = "ORDER_AMOUNT_EXCEEDS"
    OrderAlreadyPaid   OrderErrorCode = "ORDER_ALREADY_PAID"
    OrderNotFound      OrderErrorCode = "ORDER_NOT_FOUND"
)

// 编译期检查示例
func handleError(code OrderErrorCode) {
    switch code {
    case OrderAmountExceeds:
        // 业务处理
    case OrderAlreadyPaid:
        // 业务处理
    default:
        // 编译期不会漏掉新增枚举
    }
}

3.3 可扩展:错误码体系支持版本演进

业务系统会持续迭代,错误码必须支持向后兼容的扩展。设计时需考虑:

  • 命名空间隔离:不同领域(订单、支付、库存)使用独立前缀,如 ORDER_、PAY_、STOCK_。
  • 版本号嵌入:对于重大变更,可在错误码中嵌入版本标识,如 V2_ORDER_AMOUNT_EXCEEDS。
  • 保留区间:为每个领域预留 20% 的编码空间,避免未来重构时重新编号。
领域 前缀 预留区间 示例
订单 ORDER_ ORDER_9000_ ~ ORDER_9999_ ORDER_AMOUNT_EXCEEDS
支付 PAY_ PAY_8000_ ~ PAY_8999_ PAY_BALANCE_INSUFFICIENT
库存 STOCK_ STOCK_7000_ ~ STOCK_7999_ STOCK_OUT_OF_STOCK

3.4 可国际化:错误消息与错误码分离

错误码本身是语言无关的标识符,而错误消息需要支持多语言。设计上必须将“码”与“消息”解耦:

// 错误码定义(语言无关)
type I18nErrorCode string

const (
    ErrOrderAmountExceeds I18nErrorCode = "ORDER_AMOUNT_EXCEEDS"
)

// 国际化消息映射
var i18nMessages = map[string]map[I18nErrorCode]string{
    "zh-CN": {
        ErrOrderAmountExceeds: "订单金额 %v 超出单笔限额 %v",
    },
    "en-US": {
        ErrOrderAmountExceeds: "Order amount %v exceeds single limit %v",
    },
}

// 断言函数:返回错误码 + 国际化消息
func AssertOrderAmountI18n(amount, limit float64, locale string) error {
    if amount > limit {
        msgTemplate := i18nMessages[locale][ErrOrderAmountExceeds]
        return &DomainError{
            Code:    ErrOrderAmountExceeds,
            Message: fmt.Sprintf(msgTemplate, amount, limit),
        }
    }
    return nil
}
国际化错误码处理流程 断言触发 生成错误码 国际化消息模板 返回 DomainError 调用方按错误码分支 关键设计:错误码与消息分离 错误码是语言无关的标识符,消息模板按 locale 动态选择

3.5 四原则综合对比

原则 核心目标 Go 实现要点 常见反例
业务语义优先 错误码直接反映领域行为 使用领域术语命名,如 ORDER_AMOUNT_EXCEEDS E1001、ERR_INVALID_PARAM
可枚举 编译期可见,IDE 可补全 自定义类型 + const 枚举 string 类型直接定义
可扩展 支持版本演进,不破坏兼容 命名空间前缀 + 预留区间 无前缀、无预留区间
可国际化 错误码与消息解耦 错误码标识符 + 消息模板映射 错误码直接拼接中文消息
实践建议:在项目初期就建立 errors/codes.go 文件,集中管理所有领域错误码。配合 go generate 自动生成枚举文档和国际化映射表,确保团队遵守统一规范。