json.marshal忽略struct字段的根本原因是字段未导出(首字母小写),go反射机制仅能访问导出字段,故小写字段被静默跳过;嵌套结构体也需逐层导出,否则整层消失。

Go 的 json.Marshal 默认只处理可导出字段,且无法改变时间格式、枚举字符串化、字段动态计算等行为——遇到这类需求,必须实现 MarshalJSON 方法,而不是靠 tag 或中间结构体硬凑。
为什么 json.Marshal 会忽略 struct 字段
字段名首字母小写(未导出)时,json.Marshal 直接跳过,这是 Go 反射机制决定的,不是 bug。比如:
type User struct {
name string // 小写 → 永远不会出现在 JSON 中
Name string `json:"name"`
}
即使给 name 加了 json:"name" tag,也无效。只有 Name 会被序列化。
- 嵌套 struct 若含未导出字段,外层加 tag 也救不回来,必须逐层导出或整体自定义
-
json:",omitempty"对零值字段生效,但对未导出字段完全无意义 - 空 struct{} 字段若没显式实现
MarshalJSON,默认输出为{}还是被忽略,取决于它是否可导出
什么时候必须重写 MarshalJSON 和 UnmarshalJSON
标准库无法覆盖的场景,才需要手动实现这两个方法。常见触发点包括:
- 时间字段要输出
"2024-03-15"而非 RFC3339:t.Format("2006-01-02")必须在MarshalJSON里做 - 枚举类型如
type Status int,想序列化成"active"而非1,tag 无法完成映射 - 敏感字段需脱敏(如手机号显示为
"138****1234"),逻辑必须写在MarshalJSON中 - 字段值依赖其他字段动态生成(如
FullName=FirstName + " " + LastName),不能靠 struct tag - 兼容旧版 API 字段别名(如后端字段叫
CreatedAt,但老前端只认create_time),且不能全局改 tag
注意:UnmarshalJSON 里别直接调 json.Unmarshal(b, &*s),会无限递归导致栈溢出。
实现 MarshalJSON 时如何避免无限递归
最常见错误是在 MarshalJSON 方法体内直接调用 json.Marshal(u),这会再次触发同一个方法,死循环。正确做法是用类型别名绕过:
func (u User) MarshalJSON() ([]byte, error) {
type Alias User // 新类型,不带方法
raw := struct {
*Alias
CreatedAt string `json:"created_at,omitempty"`
}{
Alias: (*Alias)(&u),
CreatedAt: u.CreatedAt.Format("2006-01-02"),
}
return json.Marshal(raw)
}
-
type Alias User是关键:它剥离了User的所有方法,包括MarshalJSON - 不能写
type Alias = User(类型别名),那只是 alias,方法集不变 - 如果结构体嵌套深,每层都可能需要这种 alias 技巧,否则某一层漏掉就会递归
- 别忘了检查
u.CreatedAt.IsZero()再决定是否赋值,否则零值时间会 panic
json.RawMessage 不是万能解药,用错反而更难 debug
json.RawMessage 是延迟解析的 []byte,适合字段类型不确定的场景(比如 webhook 的 data 字段可能是 object 或 array)。但它有明确边界:
- 不能直接作为 map 的 value 类型,除非声明为
map[string]json.RawMessage - 不能放在 slice 元素里,除非是
[]json.RawMessage;否则 decode 时报cannot unmarshal object into Go value of type json.RawMessage - 它不校验 JSON 合法性,后续
json.Unmarshal失败时 panic 不捕获,容易线上 crash - 如果目的只是忽略未知字段,用
json.Decoder.DisallowUnknownFields()更轻量安全
真正需要 RawMessage 的地方很少,多数时候是开发者想绕开类型定义,结果让数据契约变得更模糊。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











