在Go语言的JSON处理场景中,嵌套结构的解析是一个高频需求,比如接口返回的JSON中包含动态结构的字段,或者嵌套的子结构类型需要根据父结构的其他字段判断。标准的json.Unmarshal方法在解码时会一次性将所有字段解析为对应的Go类型,遇到嵌套动态结构时很容易出现类型不匹配的问题,而json.RawMessage就是专门应对这类场景的标准库类型。

json.RawMessage是什么
json.RawMessage是Go语言encoding/json包中定义的一个类型,本质是[]byte的别名,它的作用是保留JSON原始编码的字节序列,不会在第一次解码时就将其转换为对应的Go结构。这意味着我们可以先解码外层结构,拿到嵌套字段的原始字节,再根据业务逻辑决定后续的解码方式,避免一次性解码带来的问题。
核心特性
- 本身是
[]byte类型,存储的是未解码的JSON原始数据 - 实现了json.Marshaler和json.Unmarshaler接口,能够正常参与JSON的编解码流程
- 支持延迟解码,只有在显式调用json.Unmarshal时才会解析内部内容
常见的嵌套JSON解码陷阱
我们先看一个没有使用json.RawMessage时容易出现的陷阱场景,假设我们有如下嵌套JSON数据,其中data字段的类型是动态的,可能是用户信息也可能是商品信息:
package main
import (
"encoding/json"
"fmt"
)
// 错误的定义方式,试图用固定结构接收动态字段
type WrongResponse struct {
Type string `json:"type"`
Data interface{} `json:"data"` // 用interface{}接收,解码后类型难以处理
}
func main() {
// 场景1:data是用户信息
jsonStr1 := `{"type":"user","data":{"name":"张三","age":20}}`
// 场景2:data是商品信息
jsonStr2 := `{"type":"product","data":{"title":"手机","price":3999}}`
var r1 WrongResponse
json.Unmarshal([]byte(jsonStr1), &r1)
// 此时r1.Data的类型是map[string]interface{},取值需要类型断言,容易出错
fmt.Printf("用户年龄类型:%Tn", r1.Data.(map[string]interface{})["age"])
}
上面的代码存在两个问题,一是用interface{}接收动态字段后,取值需要做多次类型断言,容易引发panic;二是如果data字段的结构非常复杂,一次性解码成map[string]interface{}会损耗性能,还可能丢失部分类型信息。
json.RawMessage的正确使用方式
使用json.RawMessage可以完美解决上面的问题,我们重新定义结构,将data字段声明为json.RawMessage类型:
package main
import (
"encoding/json"
"fmt"
)
// 外层响应结构,data字段用json.RawMessage接收原始字节
type Response struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// 用户结构
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
// 商品结构
type Product struct {
Title string `json:"title"`
Price int `json:"price"`
}
func main() {
jsonStr1 := `{"type":"user","data":{"name":"张三","age":20}}`
jsonStr2 := `{"type":"product","data":{"title":"手机","price":3999}}`
// 第一步:解码外层结构,拿到data的原始字节
var r1 Response
json.Unmarshal([]byte(jsonStr1), &r1)
var r2 Response
json.Unmarshal([]byte(jsonStr2), &r2)
// 第二步:根据type字段判断data的类型,再进行二次解码
if r1.Type == "user" {
var user User
// 对RawMessage存储的原始字节进行解码
json.Unmarshal(r1.Data, &user)
fmt.Printf("用户姓名:%s,年龄:%dn", user.Name, user.Age)
}
if r2.Type == "product" {
var product Product
json.Unmarshal(r2.Data, &product)
fmt.Printf("商品名称:%s,价格:%dn", product.Title, product.Price)
}
}
上面的代码执行后,会正确输出用户和商品的信息,整个过程没有类型断言,也不会出现解码类型不匹配的问题。核心逻辑是先通过json.RawMessage保留data字段的原始JSON数据,再根据外层type字段的提示,选择对应的结构进行二次解码,完全规避了动态嵌套字段的解码陷阱。
使用注意事项
1. 避免重复解码
json.RawMessage的原始数据只应该解码一次,多次解码不仅浪费性能,还可能因为原始数据被修改导致解码失败。如果需要在多个地方使用解码后的结果,应该先将解码后的结构保存下来,而不是反复对RawMessage做Unmarshal。
2. 空值与nil的区别
如果JSON中对应字段是null,那么json.RawMessage的值会是[]byte("null"),而不是nil。在判断字段是否存在或者是否为空时,需要同时判断这两种情况:
func checkRawMessage(raw json.RawMessage) bool {
if raw == nil || string(raw) == "null" {
return false
}
return true
}
3. 编码时的处理
当把包含json.RawMessage的结构重新编码为JSON时,RawMessage会直接将其存储的原始字节写入结果,不需要额外处理。但如果RawMessage中存储的不是合法的JSON,编码时会返回错误:
func testMarshal() {
r := Response{
Type: "test",
Data: json.RawMessage(`{"key":"value"}`), // 合法JSON
}
b, err := json.Marshal(r)
if err != nil {
fmt.Println("编码错误:", err)
return
}
fmt.Println(string(b)) // 输出{"type":"test","data":{"key":"value"}}
}
适用场景总结
json.RawMessage适合以下场景:
- JSON中包含动态类型字段,需要根据其他字段判断解码目标类型
- 嵌套结构非常复杂,希望延迟解码减少不必要的性能损耗
- 需要保留原始JSON数据,后续可能需要做二次校验或者转发
只要在处理嵌套JSON时遵循先解码外层、再按需解码内层的逻辑,就能充分利用json.RawMessage的特性,避免嵌套JSON解码过程中的各类陷阱。
json.RawMessageGo语言JSON解码嵌套JSON修改时间:2026-07-22 02:09:32