json.rawmessage 是处理动态结构 json 的最佳选择,它原样保存字节流、避免提前解析失败,适用于字段类型不固定或需后续校验的场景。

用 json.RawMessage 延迟解析嵌套 JSON 字段
当你面对结构不固定、字段类型动态(比如有的是 string,有的是 object)、或需要保留原始 JSON 格式做后续校验的响应时,json.RawMessage 是唯一靠谱的选择。它把一段 JSON 字节流原样存进变量,不触发反序列化,避免 panic 或静默丢字段。
常见错误是直接定义成 interface{} 或 map[string]interface{} —— 一旦响应里有非法 JSON(比如多了一个逗号)、字段值是 null 但代码假设非空,测试就会在 Unmarshal 阶段崩掉,且错误堆栈指向标准库,不好定位。
- 声明字段时用
json.RawMessage类型,不是指针也不是接口 - 测试中先检查该字段是否为空字节(
len(raw) == 0),再决定是否继续解析 - 如果要验证嵌套结构,用
json.Unmarshal(raw, &target)单独处理,错误可捕获、可断言
type ApiResponse struct {
Code int `json:"code"`
Data json.RawMessage `json:"data"` // 不解析,等测试时按需处理
}
测试 json.RawMessage 字段的三种典型场景
真实 API 返回的 data 可能是对象、数组、字符串,甚至 null。写测试不能只 mock 一种形态,否则上线后遇到其他类型就挂。
- 场景1:确认字段存在且非空 —— 断言
len(resp.Data) > 0,比!= nil更准(json.RawMessage是别名自[]byte,空值是nil切片) - 场景2:验证是合法 JSON 对象 —— 用
json.Unmarshal(resp.Data, &map[string]interface{}),捕获json.SyntaxError或json.InvalidUnmarshalError - 场景3:验证特定子字段存在且类型正确 —— 先 Unmarshal 到
map[string]json.RawMessage,再对目标 key 单独解析并断言
容易踩的坑:json.RawMessage 的零值和指针问题
很多人习惯给结构体字段加 * 指针,以为能区分“未设置”和“空”,但 json.RawMessage 是切片类型,它的零值就是 nil,加指针反而让逻辑变复杂。
立即学习“go语言免费学习笔记(深入)”;
- 不要定义成
*json.RawMessage—— 解析时会 panic:json: cannot unmarshal object into Go value of type *json.RawMessage - 反序列化后若字段缺失,
Data就是nil;若字段存在但值为null,Data是长度为 4 的字节切片([110 117 108 108]),内容是 "null" 字符串 - 测试中想区分“字段没返回”和“字段返回了 null”,得同时检查
len(raw) == 0和bytes.Equal(raw, []byte("null"))
性能与兼容性提醒
json.RawMessage 几乎零开销:它只是复制字节引用,不解析、不分配新结构。但要注意两点:
- 如果你在 HTTP handler 中把
RawMessage直接塞进响应体并调用json.Marshal,Go 会把它当普通[]byte处理,输出 base64 编码 —— 必须显式转成json.RawMessage再拼接 - Go 1.19+ 对
json.RawMessage的MarshalJSON方法做了优化,但老版本(如 1.16)在嵌套多层时可能多一次拷贝,不过对测试影响可忽略 - 别试图用
reflect.DeepEqual直接比对两个json.RawMessage—— 它们底层是切片,内容相同但地址不同也会失败;应先转string或用bytes.Equal
最麻烦的其实是调试:打印 json.RawMessage 看到的是内存地址,得手动 string(raw) 才能看到原始 JSON。这点很容易卡住人几小时。










