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