直接 json.marshal error 得到 null,因为标准 error 接口未实现 json.marshaler,且其底层类型无导出字段供反射序列化。

为什么直接 json.Marshal 一个 error 会得到 null
因为 Go 标准库的 json.Marshal 对 error 接口类型没有特殊处理,而 error 是个接口,底层具体类型(比如 *errors.errorString)通常不实现 json.Marshaler,所以默认按 nil 处理,序列化结果就是 null。
- 常见错误现象:
json.Marshal(fmt.Errorf("boom"))返回"null",不是"{\"Error\":\"boom\"}" - 根本原因:标准
error类型不满足json.Marshaler接口,也不支持反射导出字段(error的Error()方法是方法,不是可导出字段) - 不能靠加 tag 或改 struct 字段名解决——接口本身没字段
给自定义 error 类型实现 MarshalJSON 方法
最可控的方式是让自己的 error 类型显式实现 json.Marshaler 接口。注意:必须是**指针接收者**,否则值拷贝后可能丢失信息(尤其含字段时)。
- 使用场景:你封装了带状态、码、详情的 error 类型(如
type AppError struct { Code int; Message string; Cause error }) - 参数差异:
MarshalJSON()必须返回([]byte, error),内部推荐用json.Marshal序列化 map 或 struct,别手拼 JSON 字符串 - 性能影响:每次序列化都会新建 map 和调用反射,但比 panic 好;若高频日志,可预建 map 并复用
func (e *AppError) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"error": e.Message,
"code": e.Code,
"cause": e.Cause.Error(), // 注意:这里要判空,否则 panic
})
}
临时方案:用 map[string]string 包一层再序列化
如果只是调试或简单 HTTP 错误响应,不想改 error 类型定义,就绕过接口,手动转成可序列化的结构。
- 常见错误现象:直接
json.Marshal(map[string]error{"err": err})还是null—— 因为 map 的 value 类型仍是error - 正确做法:先调
err.Error()转成字符串,再塞进 map;如果需要保留原始类型信息,额外加字段如"type": "os.PathError" - 兼容性影响:无法还原 error 类型,只保留文本;对下游做类型判断的逻辑不友好
err := os.Open("/no/such/file")
data, _ := json.Marshal(map[string]string{
"error": err.Error(),
"type": fmt.Sprintf("%T", err),
})
第三方包 go-error 或 pkg/errors 不解决 JSON 序列化问题
这些包增强 error 的堆栈、包装能力,但它们的 error 类型依然没实现 MarshalJSON。想序列化?还是得自己补。
立即学习“go语言免费学习笔记(深入)”;
- 容易踩的坑:以为
pkg/errors.WithStack(err)后就能直接json.Marshal—— 实际仍是null - 如果你用了
github.com/pkg/errors,可以基于其Causer/Formatter接口写一个通用包装器,但注意:它不保证所有嵌套 error 都实现了MarshalJSON - 更安全的做法:统一用中间结构体承接,例如
type JSONError struct{ Msg string; Stack string },只在需要序列化时构造它
io.Reader),而 Go 的序列化机制不会帮你递归展开。别指望一次实现通吃所有 error 类型——按需定制才是常态。










