1. 错误码哲学:为什么需要错误码断言?领域边界与防御式编程的关系

1.1 从“错误即值”到“错误即边界”

Go 语言将错误作为一等公民——error 是一个接口,而非异常。这种设计迫使开发者显式处理每个错误,但也带来了新的挑战:错误在传递过程中会丢失语义。一个来自底层数据库的 io.EOF,经过三层业务封装后,调用方只能看到“发生了错误”,却无法判断是“数据不存在”还是“连接断开”。

错误码断言的核心哲学是:在领域边界处,将错误从“值”转化为“断言”。即,在模块的入口和出口,对错误进行结构化编码,使调用方能够通过错误码精确判断错误的性质、来源和可恢复性。

核心观点: 错误码不是日志,不是字符串,而是领域边界上的契约断言。它告诉调用方:“这个错误属于哪个域?是否可以重试?是否需要告警?”

1.2 领域边界:错误的“海关检查站”

在软件架构中,领域边界是模块、服务、层之间的分界线。每一次跨边界调用,都是错误语义丢失的高风险点。下表展示了不同边界类型下的错误传递风险:

边界类型 典型场景 错误传递风险 错误码断言价值
基础设施 → 领域 数据库查询、HTTP 调用 底层错误暴露给业务逻辑 将 io.EOF 映射为 ErrDataNotFound
领域 → 应用服务 业务规则校验、聚合操作 业务错误被包装为通用 error 通过错误码区分“参数错误”与“状态冲突”
应用服务 → 传输层 gRPC、HTTP API 响应 内部错误泄露到客户端 统一错误码格式,隐藏实现细节
进程间/服务间 微服务调用、消息队列 序列化/反序列化丢失错误类型 错误码作为稳定契约,不受语言类型影响

错误码断言正是在这些边界上建立“海关检查站”:只允许结构化的、带错误码的错误通过,而将原始的、无结构的错误拦截在边界内部。

1.3 防御式编程:在边界处“设卡”

防御式编程(Defensive Programming)强调:不要信任任何跨边界传入的数据。错误码断言是防御式编程在错误处理领域的具体实践。它要求开发者在每个领域边界处执行以下三个步骤:

  1. 断言输入:检查传入参数是否合法,若不合法则返回预定义的错误码。
  2. 断言调用结果:对下层返回的错误进行断言,将其映射为当前领域的错误码。
  3. 断言输出:确保返回给上层的错误携带正确的错误码和上下文。
实践建议: 在 Go 中,使用 errors.Is 和 errors.As 进行错误断言,而不是直接比较字符串。错误码应定义为 type ErrCode int 或 type ErrCode string,并实现 error 接口。

1.4 错误码断言 vs. 传统错误处理:对比分析

维度 传统错误处理 错误码断言
错误表示 error 接口 + 字符串消息 结构化错误码 + 可选消息
跨边界传递 逐层包装,语义稀释 错误码不变,语义稳定
调用方判断 字符串匹配或类型断言 errors.Is / errors.As 断言
可恢复性 难以区分可重试与不可重试 错误码分类明确(临时/永久)
测试性 依赖错误消息,脆弱 依赖错误码,稳定
文档化 散落在代码注释中 集中定义,可作为 API 契约

1.5 领域边界防御架构图

下图展示了在典型的三层架构中,错误码断言如何在每个领域边界发挥作用:

领域边界错误码断言架构 传输层(HTTP / gRPC) 错误码断言:将内部错误码映射为 HTTP 状态码 / gRPC 状态码 示例:ErrUserNotFound → 404 / codes.NotFound 边界1:传输层 → 应用服务 应用服务层(Use Case / Service) 错误码断言:组合领域层错误,添加业务上下文 示例:ErrInsufficientBalance → 包装为 ErrPaymentFailed 边界2:应用服务 → 领域层 领域层(Entity / Value Object / Domain Service) 错误码断言:业务规则校验,返回领域专用错误码

1.6 错误码断言的核心收益

  • 契约稳定性:错误码作为 API 契约的一部分,不会因内部实现变更而改变。
  • 可观测性:监控系统可以直接基于错误码进行告警和统计,无需解析字符串。
  • 可测试性:单元测试中,断言错误码比断言错误消息更可靠、更简洁。
  • 可恢复性:调用方可以根据错误码决定是否重试、降级或报错。
  • 安全性:避免将内部实现细节(如堆栈信息、数据库表名)泄露到外部。
常见误区: 错误码断言不是“给每个错误起个名字”。它是在领域边界上建立的结构化错误分类体系。滥用错误码(如为每个可能的分支都定义独立错误码)会导致维护成本剧增。建议每个领域模块的错误码数量控制在 10~30 个。

1.7 本章小结