go的json包默认只序列化导出字段,且对time.time、枚举等类型缺乏语义控制;自定义marshaljson/unmarshaljson必须用指针接收者,否则静默忽略,返回值须为[]byte和error,需防递归与边界异常。

为什么不能直接用 json.Marshal 和 json.Unmarshal 处理结构体字段?
因为 Go 的 json 包默认只序列化导出(首字母大写)字段,且对时间、枚举、嵌套结构等类型缺乏语义控制。比如 time.Time 默认输出 RFC3339 字符串,但你可能想要 Unix 时间戳;又比如一个字段在 JSON 中是字符串,反序列化时却要转成自定义枚举类型——这些都必须靠自定义逻辑。
实现 MarshalJSON 和 UnmarshalJSON 方法的硬性要求
Go 要求这两个方法必须定义在指针接收者上,否则运行时会静默忽略(不报错但不调用)。这是最容易踩的坑:写了方法却没生效,八成是接收者用了值类型。
-
func (t MyType) MarshalJSON() ([]byte, error)❌ 不会被json.Marshal调用 -
func (t *MyType) MarshalJSON() ([]byte, error)✅ 正确 - 返回值必须是
[]byte和error,不能省略任一 - 内部若需复用默认行为,可用
json.Marshal(&t.field),但注意避免无限递归(比如在MarshalJSON里又调用t.MarshalJSON())
处理时间字段:从 time.Time 到 Unix 秒数
常见需求是把 CreatedAt 这类字段序列化为整数时间戳,而不是 ISO8601 字符串。这时不能靠 json:"createdAt,string" 标签(它只对数字/布尔等基础类型有效),得手动控制。
type Event struct {
ID int `json:"id"`
CreatedAt time.Time `json:"-"` // 忽略原始字段
}
func (e *Event) MarshalJSON() ([]byte, error) {
type Alias Event // 防止递归调用
return json.Marshal(&struct {
CreatedAt int64 `json:"createdAt"`
*Alias
}{
CreatedAt: e.CreatedAt.Unix(),
Alias: (*Alias)(e),
})
}
这里用嵌套匿名结构体 + 类型别名是标准解法,既复用原结构体字段,又覆盖特定字段逻辑。注意 CreatedAt 在原结构体上加了 json:"-",否则会重复出现。
反序列化字符串枚举时如何做类型校验?
假设 API 返回 {"status": "active"},而你的 Go 类型是 type Status int,需要把字符串映射到常量 StatusActive = 1。这时候 UnmarshalJSON 必须自己解析字符串并验证合法性,否则容易 panic 或静默失败。
- 先用
json.Unmarshal解到临时string变量 - 用
switch映射字符串到枚举值,未匹配时返回明确错误(如fmt.Errorf("unknown status: %q", s)) - 不要用
strings.ToLower等隐式转换——API 严格区分大小写时会导致误判 - 如果字段允许空值或缺失,需额外判断
len(data) == 0或用json.RawMessage延迟解析
自定义序列化不是加个方法就完事,关键在边界:空输入、非法字符串、嵌套循环引用、指针 nil 解引用——这些地方一漏,线上就容易出 panic: invalid memory address 或静默数据丢失。











