json.RawMessage 是标准库中唯一能暂存原始 JSON 字节的类型,本质为 []byte 别名,不触发解码也不检查类型,但需手动管理生命周期,避免 nil 解析和内存引用失效。

json.RawMessage 能帮你跳过解析,但得手动控制生命周期
Go 的 json.Unmarshal 遇到结构不固定字段时会报错或丢数据,json.RawMessage 是标准库里唯一能“暂存原始字节”的类型。它本质是 []byte 别名,不触发解码,也不做类型检查——但这也意味着你得自己确保后续用对了时机和方式。
常见错误现象:panic: invalid memory address or nil pointer dereference,通常是因为把未赋值的 json.RawMessage 直接传给 json.Unmarshal;或者在父结构体被 GC 后,还试图用它解析子字段(json.RawMessage 不持有内存拷贝,只是切片引用原始缓冲区)。
- 必须在父结构体还在作用域内时完成二次解析(比如在方法内立刻处理,别存为全局变量或返回裸
json.RawMessage) - 如果要跨函数传递,建议封装成方法,例如
func (r *RawData) GetUser() (*User, error),内部做拷贝和解析 - 注意:
json.RawMessage不能直接用==比较,要用bytes.Equal
嵌套动态字段怎么定义结构体才不踩坑
当 JSON 里某字段可能是对象、数组、字符串甚至 null,又不想用 interface{}(类型断言太麻烦、性能差),就该用 json.RawMessage 占位。关键在于结构体字段声明和 tag 的配合。
使用场景:Webhook 接收方、微服务间协议兼容旧版字段、配置项支持扩展字段。
立即学习 “go 语言免费学习笔记(深入)”;
参数差异:字段必须是导出(大写开头),且 tag 中不能加 omitempty(否则空值会被忽略,导致 RawMessage 为 nil)。
type Event struct {ID string `json:"id"` Type string `json:"type"` Data json.RawMessage `json:"data"` // ✅ 正确:无 omitempty // Data json.RawMessage `json:"data,omitempty"` // ❌ 错误:null 或缺失时 Data == nil}
延迟解析时如何避免重复解码和内存泄漏
很多人以为 json.RawMessage 是“懒加载”,其实它只是“跳过解析”,原始字节仍随父结构体一起分配。如果反复对同一 json.RawMessage 调用 json.Unmarshal,每次都会重新分配目标结构体内存,没复用、也没释放旧对象。
性能影响:高频调用下 GC 压力明显上升;兼容性上,所有 Go 版本行为一致,但老版本(RawMessage 的 panic 提示更模糊。
- 推荐做法:解析后缓存结果,用指针字段 + once.Do 控制单次初始化
- 不要在循环里反复解析同一个
json.RawMessage,先提取再复用 - 如果字段内容很大且只读一次,考虑用
io.NopCloser(bytes.NewReader(rm))包装后交给其他需要io.Reader的函数,避免内存拷贝
遇到 null 或缺失字段时 RawMessage 怎么判空
json.RawMessage 对应 JSON 的 null 时,值是 nil;对应缺失字段时,也是 nil;但对应空对象 {} 时,是长度为 2 的 []byte(即 []byte("{}"))。三者语义完全不同,但都容易被当成“空”处理掉。
容易踩的坑:用 len(rm) == 0 判空,会把 null 和缺失字段当成空对象;用 rm == nil 又无法区分 null 和缺失。
- 想区分缺失 vs
null:得用指针字段 +json:",omitempty"配合自定义 UnmarshalJSON 方法 - 多数情况只需知道“有没有有效数据”:用
!json.Valid(rm)判断是否为null或非法字节,再结合rm != nil - 示例判断逻辑:
if rm != nil && len(rm) > 0 && !bytes.Equal(rm, []byte("null"))