应优先用 json.rawmessage 延迟解析动态 json:对结构不定的字段(如 data)、动态 key(map[string]json.rawmessage)或混合类型字段(自定义 unmarshaljson),先暂存原始字节,再按需解码,兼顾灵活性与类型安全。

用 json.RawMessage 延迟解析不确定层级的字段
遇到结构不固定、嵌套深度不可预知的 JSON(比如 Webhook 透传数据、第三方 API 返回的动态 schema),硬写 struct 会频繁报 json: cannot unmarshal object into Go struct field。这时候别急着用 map[string]interface{} 全量解析——它会让后续取值变得又臭又长,还失去类型安全。
更实用的做法是:对可能变化的子字段,先用 json.RawMessage 当“占位符”接住原始字节,等真正需要读取时再二次解析。这样既跳过校验失败,又保留按需解码的灵活性。
-
json.RawMessage本质是[]byte,不会触发反序列化,也不要求字段存在 - 适用于顶层字段名已知、但值结构多变的场景(例如
"data"字段可能是对象、数组或 null) - 注意:多次解析同一段
json.RawMessage是安全的,但别把它直接塞进 map 或 struct 后长期持有,避免内存泄漏(底层 byte slice 可能引用大块原始 JSON)
用 map[string]json.RawMessage 处理动态 key 名 + 未知 value 结构
当 JSON 的 key 本身是运行时生成的(如用户 ID、设备编号、指标名称),没法提前定义 struct 字段。这时 map[string]interface{} 看似方便,但一碰到数字 key(如 "123")或混合类型 value,就容易在取值时 panic。
换成 map[string]json.RawMessage 更稳:key 保持字符串,value 暂存原始 JSON 片段,后续根据业务逻辑判断类型再解析。
var payload map[string]json.RawMessage
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
// 检查某个 key 是否存在且非空
if raw, ok := payload["user_456"]; ok && len(raw) > 0 {
var user struct{ Name string }
if err := json.Unmarshal(raw, &user); err == nil {
fmt.Println(user.Name)
}
}
- 必须检查
len(raw) > 0,因为 JSON 中的null会被解成空[]byte - 不要对
json.RawMessage做字符串拼接或正则匹配——它不保证格式化,可能含换行/空格,也不适合直接string()打印调试 - 如果 key 集合有限(比如只可能是
"user"/"device"/"config"),建议先用map[string]json.RawMessage提取,再分发给对应解析函数,比全用interface{}更易维护
用自定义 UnmarshalJSON 方法应对混合类型字段
有些字段在不同请求中可能是 string、number 或 object(比如 "price" 有时是 "9.99",有时是 {"value": 9.99, "currency": "USD"})。标准库无法自动适配这种“一字段多形态”,直接定义为 interface{} 会导致后续类型断言繁琐且易错。
更干净的方式是为该字段定义一个包装类型,并实现 UnmarshalJSON 方法,在里面做类型探测和分支解析。
type Price struct {
Value float64
Currency string
IsObject bool
}
func (p *Price) UnmarshalJSON(data []byte) error {
// 先尝试解析为 object
var obj map[string]interface{}
if err := json.Unmarshal(data, &obj); err == nil && len(obj) > 0 {
p.IsObject = true
p.Value = getFloat(obj, "value")
p.Currency = getString(obj, "currency")
return nil
}
// 再尝试解析为 number 或 string-number
if v, err := strconv.ParseFloat(strings.TrimSpace(string(data)), 64); err == nil {
p.Value = v
return nil
}
return fmt.Errorf("cannot unmarshal price from %s", string(data))
}
- 注意解析顺序:先试复杂结构(object),再试简单类型(number/string),避免把
{"value":123}错当成字符串"{...}" - 务必处理
null:如果字段允许为 null,json.Unmarshal会传入data == []byte("null"),需单独判断 - 这个模式适合高频出现、规则明确的“歧义字段”,不推荐泛化到所有字段——否则代码膨胀快,可读性下降
警惕 json.Number 在数字精度和比较中的陷阱
当 JSON 包含大整数(如 17 位以上 ID)或高精度小数时,用 map[string]interface{} 解析后,数字默认变成 float64,会丢失精度。启用 Decoder.UseNumber() 可让数字以 json.Number 形式暂存,但这也带来新问题:你不能直接拿 json.Number 和 int64 比较,也不能直接加减。
- 调用
json.Number.Int64()或json.Number.Float64()前,必须用err != nil检查溢出,否则 panic -
json.Number.String()返回原始字符串(含引号?不,它去掉了引号),可用于日志或透传,但不能直接用于计算 - 如果后续要存 DB 或做数值运算,建议尽早转成确定类型(如
int64或big.Int),别让json.Number流到业务逻辑深处
多重嵌套里混用 json.RawMessage、动态 map 和自定义解码器时,最容易忽略的是错误传播路径——某个子字段解析失败,上层可能静默吞掉,导致数据缺失却无提示。留心每个 json.Unmarshal 调用点的错误处理,别只在顶层 check 一次。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











