Go 的 json.Unmarshal 依赖反射,仅处理导出字段(首字母大写),非导出字段无论有无 json 标签均被忽略;常见错误是字段未导出或标签拼写错误导致解析后仍为零值。

Go 的 json.Unmarshal 本质就是靠反射工作的
它不依赖结构体标签以外的任何元信息,但必须能通过反射访问字段——也就是说,只有导出(首字母大写)字段才会被解析。非导出字段无论有没有 json:"xxx" 标签,一律忽略。
常见错误现象:json.Unmarshal 成功返回 nil 错误,但结构体字段仍是零值。大概率是字段未导出,或标签拼写错误(比如写成 json"xxx" 少了冒号)。
- 结构体字段必须以大写字母开头
-
json:标签中的字段名要和 JSON key 完全匹配(区分大小写),除非用json:",string"强制转字符串解析 - 嵌套结构体字段同样需导出;匿名字段若导出且无冲突,会自动展开
手动用 reflect.Value.SetMapIndex 填充 map[string]interface{} 到结构体?别这么干
有人想绕过 json.Unmarshal,自己用反射逐字段赋值,典型场景是想在反序列化时做字段级预处理(如 trim 空格、转换时间格式)。但直接操作 reflect.Value 填充结构体极易 panic:比如对不可寻址的 value 调用 Set,或类型不匹配时调用 SetString。
更稳妥的做法是先用 json.Unmarshal 解到 map[string]interface{} 或 json.RawMessage,再按需处理对应字段,最后用标准方式重新解到目标结构体——或者干脆自定义 UnmarshalJSON 方法。
立即学习“go语言免费学习笔记(深入)”;
- 结构体指针传给
reflect.ValueOf才能得到可寻址的Value -
reflect.Value.SetString要求底层类型是string,且 value 必须可设置(CanSet()返回 true) - 时间、数字等类型从
interface{}转换时需类型断言,失败会 panic,建议用switch v := raw.(type)分支处理
为什么 json.Marshal 不报错但输出空对象 {}?检查反射可导出性
这通常不是反射本身的问题,而是 json.Marshal 在反射过程中发现没有可序列化的字段,就直接返回空对象。和 Unmarshal 同理:字段未导出、标签设为 -、或类型不支持(如 func、unsafe.Pointer)都会导致字段被跳过。
调试技巧:用 reflect.TypeOf(t).NumField() 看实际有多少字段参与反射;再遍历每个 StructField,检查 IsExported() 和 Tag.Get("json") 是否合理。
-
json:"-"表示完全忽略该字段,哪怕它是导出的 -
json:"name,omitempty"在字段为零值时才忽略,但前提是字段本身能被反射看到 - 内嵌结构体字段若未导出,即使外层结构体导出,也不会被序列化
自定义 UnmarshalJSON 时,json.RawMessage 是绕过默认反射逻辑的关键
当你需要完全控制反序列化过程(比如兼容多种 JSON 格式、做字段校验、或延迟解析大字段),应实现 UnmarshalJSON([]byte) error 方法。此时不能再依赖 json.Unmarshal 直接填结构体,而要用 json.RawMessage 暂存原始字节,后续按需解析——这本质上是把反射时机从入口推迟到业务逻辑中。
注意:在自定义方法里调用 json.Unmarshal 解到自身字段时,仍受前述导出规则约束;如果想解到非导出字段,只能用反射 + Set,但务必确保 value 可寻址、类型匹配、且不违反内存安全。
-
json.RawMessage是[]byte的别名,不触发任何解析,适合做中间缓存 - 在
UnmarshalJSON中用json.Unmarshal解到局部变量,再通过反射或直接赋值写入 receiver 字段 - 不要在
UnmarshalJSON里递归调用自身,容易栈溢出










