Golang中的错误处理与领域驱动设计(DDD) Go语言业务异常建模

2次阅读

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

Golang中的错误处理与领域驱动设计(DDD) Go语言业务异常建模

go 里不该用 errors.Newfmt.Errorf 建模业务异常

业务异常不是“出错了”,而是“按规则走到了这个分支”——比如余额不足、订单已取消、用户无权限。用 errors.New 造的错误无法携带领域语义,也没法被上层稳定识别和响应。

常见错误现象:写了个 if balance ,结果调用方只能靠字符串匹配判断,一改提示就崩;或者加了日志后发现所有错误都在同一个监控告警里,分不清是系统故障还是业务拒绝。

  • 业务异常必须是**可类型断言的自定义错误类型**,例如 InsufficientBalanceError
  • 每个错误类型应实现 Error() 方法,并附带结构化字段(如 OrderIDExpectedActual
  • 避免在错误中塞敏感数据(如原始密码、完整身份证号),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.IsAs 对深度嵌套的包装链响应变慢。

  • 业务错误之间不推荐用 %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 上下文决定,而不是错误本身

领域错误建模最难的不是定义结构,而是守住“错误即领域事实”的边界——它得能被业务人员理解,也能被代码精确识别。一旦开始用字符串匹配、靠日志关键词告警、或者让 error 类型承担 HTTP 职责,DDD 就已经漏气了。

text=ZqhQzanResources