Go解析嵌套JSON必须严格镜像层级,json:"a.b"标签无效;需逐层定义结构体,字段首字母大写并用json:"key"显式映射;动态键用map[string]T,混型数组用json.RawMessage延迟解析。

嵌套结构体必须严格镜像JSON层级
Go的json.Unmarshal不会自动跳层或展开路径,写json:"data.items"这种标签完全无效。它只在当前层级匹配键名,所以JSON里有几层对象,Go里就得定义几层struct。
常见错误是把{"data":{"items":[{"id":1}]}}强行塞进一个带Items []Item字段的顶层struct——这会失败,因为data本身是个对象,不是字段。正确做法是拆出Data类型:
type Response struct {
Data Data `json:"data"`
}
type Data struct {
Items []Item `json:"items"`
}
type Item struct {
ID int `json:"id"`
}
- 所有字段首字母必须大写,否则
encoding/json根本看不到 - 容器层如
"data"、"result"哪怕没业务含义,也得建模为独立类型,别试图用匿名字段“省事” - 字段名和JSON key不一致时,
json:"key_name"标签不能省;哪怕碰巧一致,也建议显式写上,避免重命名后静默失效
动态键名只能用map[string]T,别碰map[string]interface{}
遇到{"2024-01-01": {"status":"ok"}, "2024-01-02": {"status":"fail"}}这类结构,map[string]interface{}看似灵活,但后续取值要层层断言:v["2024-01-01"].(map[string]interface{})["status"],极易panic。
更稳的解法是定义语义化类型再映射:
立即学习“go语言免费学习笔记(深入)”;
type DailyStatus struct {
Status string `json:"status"`
}
// 字段类型直接设为 map[string]DailyStatus
type Report struct {
ByDate map[string]DailyStatus `json:"daily"`
}
- key是数字字符串(如
"1001")?JSON标准只允许string key,Go侧按map[string]T处理即可 - value结构也不固定?改用
map[string]json.RawMessage缓存原始字节,后续按需解析,避免重复解码 - 别把
map[string]interface{}当默认选项——它只是把类型检查和panic推到运行时
混型数组必须用json.RawMessage延迟解析
JSON数组里元素类型不一致,比如[{"type":"user","name":"A"},{"type":"admin","level":9}],硬解到[]interface{}或统一struct必然丢数据或报错。
正确姿势是先保留原始字节,再分支解析:
type Payload struct {
Items []json.RawMessage `json:"items"`
}
// 解析时:
for _, raw := range payload.Items {
var t struct{ Type string }
json.Unmarshal(raw, &t)
switch t.Type {
case "user":
var u User
json.Unmarshal(raw, &u)
case "admin":
var a Admin
json.Unmarshal(raw, &a)
}
}
-
json.RawMessage本质是[]byte别名,零拷贝,不触发二次解析 - 必须作为struct字段类型使用,不能赋值给局部变量后直接
json.Unmarshal - 别在循环里反复定义临时struct——提取成独立类型,方便复用和测试
空对象、null、类型不固定的字段要慎设类型
像"RT": null、"gtAns": true、"gtAns": "yes"这种字段,硬设string或bool会导致整个结构体解码失败。
Go没有联合类型,但可用以下方式兜底:
- 字段声明为
json.RawMessage,后续按实际内容分支处理 - 用自定义类型实现
json.Unmarshaler接口,在方法内手动判断类型并赋值 - 若仅需读取,
map[string]json.RawMessage比map[string]interface{}更安全——它保留原始字节,不触发类型转换
最易被忽略的是:空对象{}解到struct得零值,解到map[string]interface{}得空map,行为完全不同。选哪种取决于你后续逻辑是否依赖“是否存在该字段”这个语义。


















