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 结构体与断言器之间的协作关系:
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 设计为扁平结构,另一些则支持嵌套。下表对比两种方案的适用场景:
| 方案 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
扁平 Detailsmap[string]string |
序列化简单,断言时无需递归 | 无法表达数组或嵌套对象 | 简单校验错误,如字段名+原因 |
嵌套 Detailsmap[string]any |
灵活,可表达复杂结构 | 断言时需类型断言或反射 | 多字段校验、批量操作结果 |
GoBFF 推荐使用 map[string]any,因为 BFF 层经常需要透传下游微服务的复杂错误详情,扁平结构容易丢失信息。
4.6 本章小结
- BizError 结构体包含
Code、Message、Details三个字段,职责分离。 - Error() 方法实现
error接口,仅用于日志,不用于断言。 - 断言时直接读取结构体字段,避免字符串解析带来的脆弱性。
- Details 使用
map[string]any保持灵活性,适应 BFF 透传复杂错误的需求。