json.marshal只序列化可导出字段(首字母大写)且默认用字段名作key;未导出字段或无tag字段被忽略,嵌套未导出struct同理;时间、枚举、动态计算等需自定义marshaljson/unmarshaljson;json.rawmessage用于延迟解析,非万能解药;nil指针、零值、空切片等边界必须显式处理。

为什么 json.Marshal 会忽略 struct 字段
字段没导出(首字母小写)或没加 tag,json.Marshal 就直接跳过——不是 bug,是 Go 的反射规则决定的。它只序列化可导出字段,且默认用字段名作 key。
- 检查字段是否以大写字母开头:
type User struct { name string }中的name永远不会进 JSON - 用
json:"name"tag 强制暴露时,记得加omitempty控制零值行为:Age int `json:"age,omitempty"` - 空 struct{} 字段若没 tag,也会被忽略;想保留空对象,得手动实现
MarshalJSON - 嵌套 struct 若未导出,外层即使有 tag 也无效——必须逐层导出或自定义
什么时候必须重写 MarshalJSON 和 UnmarshalJSON
标准库无法处理的场景:时间格式不统一、枚举用字符串而非数字、字段需动态计算、敏感字段要脱敏、或需要兼容旧版 API 的字段别名。
- 时间字段常用
time.Time,但 API 要求"2024-03-15"而非 RFC3339,就得在MarshalJSON里调t.Format("2006-01-02") - 枚举类型如
type Status int,想序列化成"active"而非1,不能靠 tag 解决,必须手写逻辑 -
UnmarshalJSON接收字符串但要存为 int?先json.Unmarshal到临时 string 变量,再转,否则会无限递归 - 别在
UnmarshalJSON里直接调json.Unmarshal(b, &*s)——这会再次触发你的方法,栈溢出
json.RawMessage 是什么,以及它为什么不是万能解药
json.RawMessage 是延迟解析的 byte slice,帮你跳过中间结构体,把一段 JSON 原样塞进字段,等真正需要时再解。
- 适合字段类型不确定的场景,比如 webhook payload 中的
data字段可能为 object 或 array - 性能上省了两次拷贝和反射,但代价是你得自己保证后续解析安全——
json.Unmarshal失败时 panic 不会自动捕获 - 不能直接用在 map value 或 slice element 上(除非显式声明为
[]json.RawMessage),否则 decode 会报cannot unmarshal object into Go value of type json.RawMessage - 如果只是想忽略未知字段,用
json.Decoder.DisallowUnknownFields()更轻量,RawMessage是为“稍后处理”设计的,不是“绕过校验”的快捷键
自定义编解码时最常漏掉的边界:nil 指针与零值
struct 字段是 *T 类型时,nil 指针在 MarshalJSON 中不等于零值,但很多人忘了判空,导致 panic 或空字符串。
立即学习“go语言免费学习笔记(深入)”;
-
if s.Name == nil再 returnjson.Marshal(nil),否则(*string).MarshalJSON会 panic - 接收方
UnmarshalJSON如果返回nil,Go 会认为成功,但字段仍为零值——必须显式赋值,比如*s = MyType{} - 切片字段为
[]int时,空切片[]和nil在 JSON 中都编码为[],但语义不同;若业务要求区分,得用*[]int并在方法里判断指针是否为 nil - 所有自定义方法必须满足:对同一输入,
MarshalJSON→UnmarshalJSON后值应等价;测试时别只测非空,一定要加nil和零值用例










