go json反序列化中nil指针字段保持nil,需用*string等指针类型并保持未初始化才能赋值;区分“缺失”与“null”须用json.rawmessage或自定义unmarshaljson。

Go JSON反序列化时 nil 指针字段不会被赋值,除非显式声明为指针类型
JSON 解析器(json.Unmarshal)默认只填充结构体中「已初始化」的字段。如果字段是 *string、*int 这类指针类型,但结构体实例里该字段是 nil,反序列化时它依然保持 nil —— 即使 JSON 里有对应 key 和非空值。
常见错误现象:json.Unmarshal 后发现 *string 字段还是 nil,但日志打印 JSON 明明有这个字段;或者用 omitempty 导致字段被跳过,误以为数据丢了。
- 必须把字段定义成指针类型(如
*string),且不预先初始化(保持nil),才能让json.Unmarshal在遇到 JSON 中该字段时「分配新内存并写入」 - 如果字段是
string(非指针),JSON 为空字符串""或缺失,都会变成零值"",无法区分「没传」和「传了空字符串」 -
json.RawMessage可临时绕过解析,适合延迟判断字段是否存在,但需手动再解一次
用 json.RawMessage + 延迟解析判断字段是否真实存在
当必须严格区分「JSON 中无该字段」和「字段值为 null / 零值」时,json.RawMessage 是最直接的手段:它把原始字节原样存下来,不触发类型转换。
使用场景:API 兼容旧版字段可选、配置项动态生效、审计日志需保留原始输入结构。
立即学习“go语言免费学习笔记(深入)”;
- 字段类型设为
json.RawMessage,反序列化后检查其长度:len(raw) == 0表示字段缺失;raw == []byte("null")表示显式传了null - 后续用
json.Unmarshal(raw, &target)做二次解析,注意处理错误(比如类型不匹配) - 别直接对
json.RawMessage做比较或打印,它不是字符串;转string(raw)仅用于调试
type Config struct {
Name json.RawMessage `json:"name"`
}
// 解析后:
if len(cfg.Name) == 0 {
// 字段根本没出现
} else if string(cfg.Name) == "null" {
// 显式传了 null
} else {
var name string
json.Unmarshal(cfg.Name, &name) // 正常解析
}
UnmarshalJSON 方法里手动控制 nil 赋值逻辑
标准 json.Unmarshal 对指针字段的「赋值时机」很保守,有时你需要更细粒度的控制:比如字段为 null 时置 nil,有值时才 new;或者忽略 null 当作缺失处理。
性能影响小,但会增加维护成本;适用于核心模型或需要统一空值语义的场景。
- 为结构体实现
UnmarshalJSON([]byte) error方法,在里面用json.Unmarshal先解析到 map 或中间结构 - 检查 key 是否存在、值是否为
json.Null(即nil),再决定是否 new 指针或保持nil - 别忘了调用
json.Unmarshal原始字节到具体字段,否则字段不会被填充
嵌套结构体中指针字段的零值陷阱
如果外层结构体字段是指向结构体的指针(如 *User),而 JSON 里该字段是 null,反序列化后它就是 nil —— 这没问题;但如果 JSON 里完全没这个字段,它也还是 nil。两者行为一致,但语义不同。
容易踩的坑:用 if user != nil 判断字段是否存在,结果把「没传」和「传了 null」当成一回事;下游代码 panic 或逻辑错乱。
- 想区分二者,必须在外层用
json.RawMessage捕获,或在自定义UnmarshalJSON中解析map[string]json.RawMessage - 别依赖指针是否为
nil来推断业务含义;加注释说明该字段的空值语义(例如:“nil表示未提供,&User{}表示提供但为空对象”) - 测试时务必覆盖三种 case:字段缺失、字段为
null、字段有有效值
nil 到底代表「用户没填」,还是「用户明确清空」,或是「传输出错了」。










