许多 Go 开发者在调用 json.Marshal 时,会发现自定义的 MarshalJSON 方法没有被触发,或者序列化结果与预期相差甚远。根本原因在于:json.Marshal 只会调用那些直接实现了 json.Marshaler 或 json.Unmarshaler 接口的方法。如果方法采用指针接收器实现,但传入的是值类型,或者结构体中包含未导出或不支持类型的字段,那么序列化要么不生效,要么直接报错。

为什么 json.Marshal 不调用你的方法?
这是 Go 新手最容易踩到的陷阱之一。需要明白,Go 的 JSON 序列化仅识别两个接口——json.Marshaler 和 json.Unmarshaler,并且只有当类型直接实现这两个接口时才会生效。最常见的错误场景是:为指针类型实现了接口,但传给 json.Marshal 的却是值类型的变量,此时自定义方法完全不会被调用。
举个例子,定义了 func (t *MyType) MarshalJSON() ([]byte, error),但传入的是 MyType{}(值而非指针),那么 MarshalJSON 不会执行。
- 首先检查接收器类型:值接收器适用于值变量,指针接收器适用于指针变量或取地址后的值
- 接着确认变量实际类型:使用
fmt.Printf("%T", v)查看传入的是MyType还是*MyType - 当结构体字段为值类型时,嵌套嵌入也要确保接收器一致性,否则嵌套序列化会跳过自定义逻辑
如何让时间字段输出为 "2006-01-02" 而不是 RFC3339?
标准库 time.Time 的 MarshalJSON 默认生成带时区和纳秒的完整 RFC3339 格式字符串,但实际业务场景中通常只需要简洁的日期格式,例如 "2006-01-02"。由于标准库类型不可修改,不能直接改动 time.Time,因此需要包装一层自定义类型。
type Date time.Time
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(`"` + time.Time(d).Format("2006-01-02") + `"`), nil
}
// 用法:struct 中字段类型声明为 Date,不是 time.Time
type Event struct {
When Date `json:"when"`
}
- 务必确保返回的字节包含双引号,因为
json.Marshal不会自动添加 - 不要遗漏
time.Time(d)类型转换,否则无法调用Format方法 - 若需要反序列化,则必须同时实现
UnmarshalJSON,并正确处理带引号的字符串输入
嵌套结构体里部分字段想忽略,但又不想用 json:"-"?
全局忽略使用 tag 是最便捷的方式,但有时逻辑更复杂,比如希望只在特定状态(如 Status == "draft")下才忽略某个字段。这种情况下 tag 无法满足需求,需要通过自定义 MarshalJSON 来手动控制字段的输出与否。
func (u User) MarshalJSON() ([]byte, error) {
type Alias User // 防止无限递归
raw := struct {
*Alias
CreatedAt string `json:"created_at,omitempty"`
}{
Alias: (*Alias)(&u),
}
if u.Status != "draft" {
raw.CreatedAt = u.CreatedAt.Format(time.RFC3339)
}
return json.Marshal(raw)
}
- 关键在于使用
type Alias User,以避免递归调用自身的MarshalJSON导致死循环 - 字段名的大小写必须与目标 JSON key 完全一致,否则对应字段不会出现在输出中
- 避免在
MarshalJSON中直接拼接 JSON 字符串,这种方法易出错且难以维护;推荐优先使用 map 或匿名 struct 路线
为什么实现了 MarshalJSON 却仍报 json: unsupported type?
您可能会疑惑:明明实现了接口,为什么仍然报错?实际上,这个错误通常不是因为接口未实现,而是因为底层字段类型不被 json.Marshal 所支持,例如字段为 func、chan、map[interface{}]interface{},或者是未导出的字段(小写字母开头)。
- 检查结构体中的所有字段是否可导出(首字母大写),否则即便实现了接口,内部字段也无法通过反射访问
- 确认没有嵌入不支持序列化的类型,例如
http.Handler、sync.Mutex等,它们本身不可序列化 - 若使用
map[string]interface{}存储动态数据,需确保值类型都在 JSON 支持范围内(string、int、float、bool、map、slice、nil) - 对于第三方类型(如
uuid.UUID),需自行实现MarshalJSON,或利用其自带序列化方法转换为字符串后再存储
最稳妥的方案是:在自定义 MarshalJSON 中显式列出需要序列化的字段,而不是依赖反射自动遍历。这既能避免 unsupported type 报错,又能精确控制输出内容。
