业务异常不应使用 errors.new 或 fmt.errorf,因其无法携带领域语义且难以被稳定识别;应视为规则分支而非错误,如余额不足、订单已取消等。

Go 里不该用 errors.New 或 fmt.Errorf 建模业务异常
业务异常不是“出错了”,而是“按规则走到了这个分支”——比如余额不足、订单已取消、用户无权限。用 errors.New 造的错误无法携带领域语义,也没法被上层稳定识别和响应。
常见错误现象:写了个 if balance ,结果调用方只能靠字符串匹配判断,一改提示就崩;或者加了日志后发现所有错误都堆在同一个监控告警里,分不清是系统故障还是业务拒绝。
- 业务异常必须是**可类型断言的自定义错误类型**,例如
InsufficientBalanceError - 每个错误类型应实现
Error()方法,并附带结构化字段(如OrderID、Expected、Actual) - 避免在错误中塞敏感数据(如原始密码、完整身份证号),DDD 要求错误只暴露必要上下文
如何让错误类型参与领域行为决策
DDD 的核心是把业务规则内聚到领域对象里,错误类型得能触发对应策略,而不是被当成失败信号丢弃。
使用场景:支付服务调用风控接口后返回 RiskRejectedError,下游不能只记日志,而要自动触发人工复核流程或降级为短信验证。
立即学习“go语言免费学习笔记(深入)”;
- 定义错误时嵌入领域状态字段,例如
type RiskRejectedError struct { OrderID string; Reason RiskReason } - 在 handler 或 application service 层用
errors.As判断错误类型,而非strings.Contains - 错误类型本身可带方法,比如
func (e RiskRejectedError) ShouldEscalate() bool { return e.Reason == HIGH_RISK }
为什么 fmt.Errorf("...: %w") 在领域错误链里很危险
包装错误(%w)适合传递底层技术错误(如数据库超时),但会模糊业务错误的边界。一个 PaymentFailedError 被层层包装后,最终可能被当成网络问题重试三次,而它本意是“银行卡已过期”,重试毫无意义。
性能影响:每次 %w 都会拷贝栈帧,对高频业务路径有可观开销;兼容性上,Go 1.20+ 的 errors.Is 和 As 对深度嵌套的包装链响应变慢。
- 业务错误之间不推荐用
%w包装,宁可用组合字段表达上下文,例如PaymentFailedError{Original: CardExpiredError{CardNo: "****1234"}} - 只有当需要保留底层技术错误(如
*pq.Error)供运维诊断时,才在最外层做一次包装 - 测试时用
errors.Is(err, myDomainErr)断言,别依赖errors.Unwrap遍历链
HTTP API 返回码与领域错误的映射不能硬编码
一个 OrderNotFound 在查询接口返回 404 合理,但在创建接口里可能是 400(参数非法),硬写 if errors.Is(err, ErrOrderNotFound) { return 404 } 会污染领域层,也违背 DDD 分层隔离原则。
正确做法是在接口层(transport 或 handler)做映射,且映射逻辑要可配置、可测试。
- 定义错误到状态码的映射表,例如
map[error]int{&OrderNotFoundError{}: 404, &InsufficientBalanceError{}: 400} - 避免在 error 类型里加 HTTP 相关字段(如
StatusCode),那会让领域模型耦合传输协议 - 对同一错误类型,在不同 endpoint 可返回不同状态码,靠 handler 上下文决定,而不是错误本身











