go 的 encoding/json 和 gin 不支持将 json null 直接映射到非指针 go 类型,必须用指针类型(如 int、string)接收才能避免 panic 或丢数据;但指针无法区分 null 与字段缺失,需通过自定义 unmarshaljson 或额外标记字段实现三态语义。

Go 的 encoding/json 包本身不支持 JSON null 作为原生值映射到普通 Go 类型(如 int、string、bool),Gin 也继承这一限制——没有“标准规范”能让你直接把 JSON 中的 null 安全赋给非指针字段,必须显式处理。
为什么 null 会导致 panic 或静默丢数据
当你用 c.ShouldBindJSON(&v) 绑定一个结构体,而请求中某个字段是 null,但结构体字段是值类型(比如 int、string)时:
- 若字段无
omitempty且无默认值,json.Unmarshal会尝试将null赋给该字段 → 触发invalid character 'n' looking for beginning of value类错误(实际是类型不匹配) - 若字段有
omitempty,null会被跳过,字段保持零值(0、""、false),但你无法区分“前端传了null”和“前端根本没传这个字段” - 若字段是
*int、*string等指针类型,null会被正确解码为nil指针 —— 这才是唯一可靠识别null的方式
ShouldBindJSON 遇到 null 字段的正确写法
必须使用指针字段 + 显式判断 nil:
type UpdateUserRequest struct {
ID *int64 `json:"id"`
Name *string `json:"name"`
Active *bool `json:"active"`
}
func updateUser(c *gin.Context) {
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 分别检查是否为 nil,即是否传了 null
if req.ID != nil {
// 处理 ID 更新逻辑
fmt.Printf("ID set to: %d\n", *req.ID)
} else {
// 前端传了 "id": null,或根本没传 id 字段 → 无法区分
// 若需区分,必须配合自定义 UnmarshalJSON 或额外标记字段
}
}
注意:nil 指针既可能来自 "id": null,也可能来自字段完全缺失 —— Gin 和标准库不做区分。
想严格区分 null 和缺失字段?得自己实现 UnmarshalJSON
标准库不提供“三态”语义(present / absent / null),要精确控制,必须手动实现反序列化逻辑:
type NullableInt struct {
Value *int64
Set bool // true 表示字段存在(无论 null 还是数字)
}
func (n *NullableInt) UnmarshalJSON(data []byte) error {
n.Set = true
if string(data) == "null" {
n.Value = nil
return nil
}
var v int64
if err := json.Unmarshal(data, &v); err != nil {
return err
}
n.Value = &v
return nil
}
type UpdateRequest struct {
ID NullableInt `json:"id"`
}
这样就能在业务层明确知道:ID.Set == false → 字段未传;ID.Set && ID.Value == nil → 字段传了 null;ID.Value != nil → 有具体值。
Gin 返回响应时的 null 控制
响应侧不需要特殊处理:只要字段是 nil 指针,c.JSON 就自动输出 null;若用 omitempty 标签,则 nil 指针会被省略(不输出该键):
-
ID *int64 `json:"id"`→ID为nil时输出"id": null -
ID *int64 `json:"id,omitempty"`→ID为nil时不输出id字段 -
ID int64 `json:"id,omitempty"`→ 即使设为0,也会被省略(因零值触发omitempty)
真正容易被忽略的是:前端传 null 后,后端若用值类型接收,不仅收不到 null,还可能因类型不匹配直接报错;而用指针虽能接住 null,却无法与“字段未传”解耦 —— 这个边界必须由你用额外字段或自定义反序列化来划清。











