4. 业务错误码结构体设计:Code、Message、Details 与 Error() 实现

在 GoBFF 架构中,业务错误码不是简单的整数枚举,而是一个携带上下文的结构体。设计良好的错误码结构体能让断言代码直接读取结构化字段,而非解析字符串。本章定义核心类型 BizError,并实现 error 接口。

4.1 结构体字段设计

业务错误码需要承载三个核心信息:机器可读的代码、人类可读的消息、以及可选的调试细节。下表列出各字段的职责与约束:

字段 类型 必填 说明
Code int 是 业务唯一错误码,用于断言匹配。例如 1001 表示用户不存在。
Message string 是 面向用户的简短提示,如“用户未找到”。
Details map[string]any 否 携带额外上下文,如请求 ID、字段名、校验失败原因。断言时可选择性验证。
设计原则:Code 用于程序逻辑判断,Message 用于日志和用户展示,Details 用于调试和链路追踪。三者职责分离,避免将业务信息硬编码到字符串中。

4.2 结构体定义与 Error() 方法

让 BizError 实现 Go 标准 error 接口,只需挂载 Error() string 方法。该方法返回一个可读的字符串,用于日志输出,但不建议在断言中依赖其格式。

// BizError 业务错误码结构体
type BizError struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Details map[string]any    `json:"details,omitempty"`
}

// Error 实现 error 接口,返回人类可读的错误描述
func (e *BizError) Error() string {
    if len(e.Details) > 0 {
        return fmt.Sprintf("[%d] %s | details: %+v", e.Code, e.Message, e.Details)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
关键点:Error() 方法仅用于日志和调试,断言时应当直接读取 .Code 字段,而非对 Error() 返回值做字符串匹配。这能避免因格式调整导致断言失效。

4.3 结构体关系与断言流程

下面的 SVG 图展示了 BizError 结构体与断言器之间的协作关系:

BizError 结构体与断言协作图 BizError 结构体 Code: int Message: string Details: map[string]any Error() string 断言器 (Assertor) AssertCode(expectedCode) AssertMessage(contains) AssertDetails(key, value) 直接读取字段,不解析字符串 断言器直接读取 Code / Message / Details,不依赖 Error() 字符串格式

4.4 使用示例:构造与断言

以下代码演示如何构造一个 BizError 实例,并在测试中对其字段进行断言:

// 构造业务错误
err := &BizError{
    Code:    1001,
    Message: "用户未找到",
    Details: map[string]any{
        "user_id": "u_abc123",
        "reason":  "数据库记录不存在",
    },
}

// 断言 Code
if err.Code != 1001 {
    t.Errorf("期望 Code=1001,实际 %d", err.Code)
}

// 断言 Message 包含关键字
if !strings.Contains(err.Message, "未找到") {
    t.Errorf("Message 应包含 '未找到',实际 %s", err.Message)
}

// 断言 Details 中的字段
if err.Details["user_id"] != "u_abc123" {
    t.Errorf("Details user_id 不匹配")
}
避坑提醒:不要在断言中使用 err.Error() 做字符串包含判断。因为 Error() 的格式可能随版本变化,且 Details 的 map 迭代顺序不确定,导致断言不稳定。始终直接访问结构体字段。

4.5 字段设计对比:扁平 vs 嵌套

有些团队会将 Details 设计为扁平结构,另一些则支持嵌套。下表对比两种方案的适用场景:

方案 优点 缺点 推荐场景
扁平 Details
map[string]string
序列化简单,断言时无需递归 无法表达数组或嵌套对象 简单校验错误,如字段名+原因
嵌套 Details
map[string]any
灵活,可表达复杂结构 断言时需类型断言或反射 多字段校验、批量操作结果

GoBFF 推荐使用 map[string]any,因为 BFF 层经常需要透传下游微服务的复杂错误详情,扁平结构容易丢失信息。

4.6 本章小结

  • BizError 结构体包含 Code、Message、Details 三个字段,职责分离。
  • Error() 方法实现 error 接口,仅用于日志,不用于断言。
  • 断言时直接读取结构体字段,避免字符串解析带来的脆弱性。
  • Details 使用 map[string]any 保持灵活性,适应 BFF 透传复杂错误的需求。