go 的 json.unmarshal 默认将 json null 映射为零值而非 nil,导致无法区分字段缺失、为 null 或存在但全零;应使用 *user 或自定义 null 类型(如 nullstring)并实现 unmarshaljson/marshaljson,避免语义混淆与运行时 panic。

Go 的 json.Unmarshal 默认把 JSON null 映射成零值,不是 nil
这是最常被误读的一点:当 API 返回 {"user": null},而你用 type Resp struct { User User } 去解码,User 字段不会是 nil,而是 User{}(即所有字段取零值)。这导致你无法区分“用户字段缺失”“用户字段为 null”“用户存在但字段全为零”三种情况。
正确做法是用指针或 sql.Null* 类型的变体(如 *User),让 Go 能表达“不存在”语义:
type Resp struct {
User *User `json:"user"`
}
这样遇到 "user": null 时,Resp.User 就是 nil;遇到 "user": {} 才会初始化一个空 User 实例。
用 sql.NullString 风格自定义类型处理可空基础字段
对于字符串、数字、布尔这类基础类型,直接用 *string 虽然可行,但容易在业务逻辑里反复判空,也藏不住“本该非空却为 null”的语义。更稳妥的是仿照 sql.NullString 自定义类型:
立即学习“go语言免费学习笔记(深入)”;
type NullString struct {
String string
Valid bool
}
func (ns *NullString) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
ns.Valid = false
return nil
}
return json.Unmarshal(data, &ns.String)
}
- 它显式暴露
Valid字段,避免业务层靠!= nil猜意图 - 注意:必须实现
UnmarshalJSON,否则嵌套结构中仍会静默转为零值 - 别忘了同时实现
MarshalJSON,否则序列化回 JSON 时可能出错
第三方库 guregu/null 的坑:不兼容 omitempty 且默认不导出字段
很多人图省事用 guregu/null.String,但它有个隐蔽行为:其内部 String 字段是小写开头(string),导致 json.Marshal 默认忽略它,即使你写了 json:",omitempty" 也没用——因为字段不可导出。
实际效果是:guregu/null.String{Valid: false} 序列化出来是空对象 {},而不是预期的省略或 null。
- 要么改用它提供的
MarshalJSON()方法手动控制 - 要么换用
github.com/lib/pq.NullString(仅限 PostgreSQL 场景) - 或者干脆自己写轻量版,字段名大写 + 显式实现编解码
API 响应中混合 null / missing / zero 的调试技巧
当上游返回混乱数据(比如有的字段是 null,有的压根没字段,有的传了 0 或 ""),光看日志很难定位。建议在开发期加一层“透明解码器”:
func DebugUnmarshal(data []byte, v interface{}) error {
fmt.Printf("Raw JSON: %s\n", data)
return json.Unmarshal(data, v)
}
再配合结构体字段加 tag 注释,例如:
type User struct {
ID int64 `json:"id"` // required, never null
Name *string `json:"name"` // optional, may be null
Email NullString `json:"email,omitempty"` // nullable + omit when empty
}
字段语义越明确,后续维护时越不容易把 nil 当 "" 用,或者把 0 当“未设置”处理。
真正麻烦的从来不是怎么写 UnmarshalJSON,而是团队里有人悄悄把 *string 解引用成 string 再传给下游,结果 panic 发生在凌晨三点的告警里。










