json.marshal 将 error 字段序列化为 null,因其不支持 error 类型且未实现 json.marshaler 接口;解决方式包括改用 string 字段或为结构体实现 marshaljson 方法。

为什么 json.Marshal 会把 error 字段序列化成 null
Go 的 json 包默认不支持直接序列化 error 类型——它既不是基本类型,也没有实现 json.Marshaler 接口。所以当你结构体里有个 err error 字段,json.Marshal 会安静地把它变成 null,连 warning 都没有。
这不是 bug,是设计:error 是运行时状态,不该默认暴露给 JSON API。但调试、日志或内部服务间通信时,你确实需要看错误内容。
- 常见错误现象:
{"msg":"failed","err":null},而你期望是{"msg":"failed","err":"connection refused"} - 别试图用指针绕过:
*error依然不行,底层还是error接口 - 别在字段上加
json:",string"tag——这只会触发字符串 marshal,但error没实现encoding.TextMarshaler,直接 panic
让 error 字段可序列化的两种可靠方式
核心思路只有两个:要么换字段类型,要么让结构体自己控制序列化逻辑。前者简单直接,后者更灵活但需谨慎。
- 方式一:用
string字段替代error,手动赋值err.Error()(适合只读场景)
→ 优点:零额外依赖,兼容所有 Go 版本
→ 缺点:丢失原始 error 类型、堆栈、causes,无法反序列化回 error - 方式二:为结构体实现
json.Marshaler接口,在MarshalJSON方法里显式处理error字段
→ 优点:可保留原始 error(比如用fmt.Sprintf("%+v", err)输出带栈信息的字符串)
→ 缺点:每个含 error 的结构体都要写一遍;若嵌套深,得递归处理
示例(方式二):
立即学习“go语言免费学习笔记(深入)”;
func (r Result) MarshalJSON() ([]byte, error) {
type Alias Result // 防止无限递归
aux := struct {
Err string `json:"err,omitempty"`
Alias
}{
Err: "",
Alias: (Alias)(r),
}
if r.Err != nil {
aux.Err = r.Err.Error() // 或 fmt.Sprintf("%+v", r.Err)
}
return json.Marshal(&aux)
}
想保留 error 类型信息?用自定义 error 类型 + MarshalJSON
如果下游需要区分不同错误(比如前端要根据 err_code 做 UI 提示),光输出字符串不够。这时应该让 error 本身可序列化。
- 定义一个实现了
json.Marshaler的 error 类型,例如APIError - 避免直接嵌入
error接口字段——会导致循环引用或空指针 panic - 推荐结构:用字段存 code、message、details,再提供
Error()方法返回 message,同时实现MarshalJSON() - 注意:标准库
errors.Unwrap和fmt.Errorf("...%w")产生的 wrapped error,在MarshalJSON里不会自动展开,得手动递归处理或放弃包装链
示例关键片段:
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e *APIError) Error() string { return e.Message }
func (e *APIError) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}{e.Code, e.Message, e.Details})
}
别忽略的边界情况:nil error、嵌套 error、第三方 error
实际代码里 error 往往不是直白的 errors.New,容易踩坑的地方集中在三类值上。
-
nilerror:必须显式判空,否则err.Error()panic;fmt.Sprintf("%v", nil)输出<nil></nil>,不是空字符串 - 嵌套结构里的 error 字段:比如
struct{ Data map[string]interface{} }中的Data["err"] = someErr—— 这种动态 map 不会触发结构体的MarshalJSON,仍会变null - 第三方 error(如
pgconn.PgError):它们通常有额外字段(SQLState,Severity),直接.Error()会丢信息;建议封装一层,提取关键字段再序列化
最稳妥的做法不是“统一处理所有 error”,而是明确哪些 error 真正需要暴露、以什么粒度暴露。JSON 是数据交换格式,不是 debug 工具——该省略的 stack trace,就别塞进去。










