Go JSON null、缺失字段和零值怎样区分
来源:17golang原创
时间:2026-10-31 19:11:51 138浏览 收藏
在 Go 里,JSON 的“字段缺失”、显式 null 和数字 0 不是同一件事;但如果直接解码到普通的 int 字段,三者很容易都表现成 0。解决办法取决于接口是否需要保留字段存在性:只关心 null 与非 null 时用指针,需要判断键是否出现时用 json.RawMessage,需要完整保留缺失、null、有效零值时用带状态的自定义类型。
- 新建的普通值结构体中,缺失字段保持零值;
null解码到普通值类型通常不改变原值;JSON 数字0则明确写入零值。 *int可以把有效的0与null区分开,但缺失和null在全新结构体里都会是nil。- 要表达三态,必须额外记录“键是否出现”,不能只看字段的最终值。
默认解码为什么会丢掉状态
先看最小结构体。下面的代码分别解码三个 JSON 对象,并打印普通 int 字段:
package main
import (
"encoding/json"
"fmt"
)
type Request struct {
Count int `json:"count"`
}
func main() {
inputs := []string{`{}`, `{"count":null}`, `{"count":0}`}
for _, input := range inputs {
var req Request
// 每次都新建结构体,避免上一次请求留下字段状态。
if err := json.Unmarshal([]byte(input), &req); err != nil {
// 输入不是合法 JSON 时立即报告错误。
panic(err)
}
fmt.Printf("%-18s => Count=%d\n", input, req.Count)
}
}
在“全新结构体”的前提下,三次输出都可能是 Count=0。缺失键没有可写入的目标,字段保持 Go 零值;null 对普通数值字段没有效果;数字 0 才是一次明确的赋值。最终值相同,并不代表输入语义相同。

先用 *int 区分有效零值和 null
如果业务只需要知道“客户端是否明确传入了一个数”,可以把字段改成指针。非 null 的 JSON 数字会分配目标值,即使数字是 0;显式 null 会把指针设为 nil。
type PointerRequest struct {
Count *int `json:"count"`
}
func decodePointer(input string) (string, error) {
var req PointerRequest
// 全新结构体下,缺失键和 null 都从 nil 开始。
if err := json.Unmarshal([]byte(input), &req); err != nil {
return "", err
}
if req.Count == nil {
// nil 可能表示缺失,也可能表示显式 null,当前类型无法再细分。
return "nil(缺失或 null)", nil
}
// 指针非 nil 时,0 仍然是一个明确传入的有效值。
return fmt.Sprintf("value=%d", *req.Count), nil
}
指针的优点是能表达“没有数值”和“数值为 0”。它的边界也很重要:在全新结构体里,字段缺失不会调用任何解码动作,显式 null 又会把指针设为 nil,所以二者仍然无法仅靠 *int 区分。
用 json.RawMessage 先判断键是否存在
要区分缺失与显式 null,可以先把对象解码成 map[string]json.RawMessage。map 的第二个返回值记录键是否出现,而 RawMessage 保留该字段的原始 JSON。
package main
import (
"bytes"
"encoding/json"
"fmt"
)
func readCount(input string) (string, error) {
var fields map[string]json.RawMessage
// 先保留字段级原文,不急着把 null 转成 Go 零值。
if err := json.Unmarshal([]byte(input), &fields); err != nil {
return "", err
}
raw, exists := fields["count"]
if !exists {
// map 的第二个返回值表示 JSON 键根本没有出现。
return "missing", nil
}
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
// 键存在但值为 null,这与 missing 是两种输入。
return "null", nil
}
var value int
// 只有确认不是 null 后才按业务类型解码具体数值。
if err := json.Unmarshal(raw, &value); err != nil {
return "", fmt.Errorf("count 类型错误: %w", err)
}
return fmt.Sprintf("value=%d", value), nil
}
这种方式适合少量可选字段或需要根据原文决定后续类型的接口。缺点是调用方需要自己维护字段解析逻辑;如果每个请求都有很多三态字段,继续堆叠 map 判断会让业务代码变得重复。
用包装类型完整保留缺失、null 和有效零值
更稳定的接口模型是把三种状态直接放到一个类型里:Set 表示键出现过,Valid 表示出现的值不是 null,Value 才保存具体数字。注意,缺失字段不会调用字段类型的 UnmarshalJSON;因此新建结构体时 Set=false 就自然代表缺失。
type OptionalInt struct {
Value int
Set bool
Valid bool
}
func (o *OptionalInt) UnmarshalJSON(data []byte) error {
o.Set = true
data = bytes.TrimSpace(data)
if bytes.Equal(data, []byte("null")) {
// null 是已出现但没有有效数值,不把它误判成缺失。
o.Value = 0
o.Valid = false
return nil
}
var value int
// 具体类型仍交给 encoding/json,保留其类型错误语义。
if err := json.Unmarshal(data, &value); err != nil {
return err
}
o.Value = value
o.Valid = true
return nil
}
type StrictRequest struct {
Count OptionalInt `json:"count"`
}
使用这个类型时可以得到明确的三态结果:
| JSON 输入 | Set | Valid | Value | 语义 |
|---|---|---|---|---|
{} | false | false | 0 | 缺失 |
{"count":null} | true | false | 0 | 显式 null |
{"count":0} | true | true | 0 | 有效零值 |

复用结构体、默认值和 omitempty 的边界
如果一个结构体会被重复解码,必须特别小心“缺失字段不会覆盖旧值”。例如第一次请求把 Count 设为有效值,第二次请求缺失 count,普通解码并不会自动把它清零;自定义包装类型的 Set 和 Valid 也不会因为字段缺失而自动复位。最简单的做法是每次请求使用新结构体,或在复用前显式重置整个对象。
omitempty 只影响 Go 值编码为 JSON 时是否省略字段,不是反序列化阶段的存在性检测器。需要向外输出三态时,通常还应明确约定:缺失是否省略、null 是否保留、有效零值是否必须输出,而不是把这些决定交给默认标签行为。
按需求选择实现方式
| 需求 | 建议类型 | 能区分什么 |
|---|---|---|
| 只要普通默认值 | int、string 等值类型 | 不保留输入存在性 |
| 区分有效 0 与 null | *int、*string | 非 null 与 null;缺失和 null 仍可能相同 |
| 少量字段需要检查原文 | map[string]json.RawMessage | 键是否出现、是否为 null、原始值 |
| 大量接口字段需要稳定三态 | 自定义包装类型 | 缺失、null、有效零值全部保留 |
判断顺序可以固定成一句话:先问接口是否关心“键有没有出现”,再问是否要区分 null,最后才选择普通值、指针、RawMessage 或包装类型。这样就不会把“最后读到的 Go 零值”误当成完整的输入语义。
常见问题
普通 int 字段能判断客户端有没有传 0 吗?
不能。缺失字段和显式 0 都可能得到 0;需要把字段改成指针或保存原始字段。
把 int 改成 *int 就能区分缺失和 null 吗?
不能保证。全新结构体中两者都可能是 nil;要区分它们,需要键存在性记录或三态包装类型。
为什么自定义 UnmarshalJSON 没有处理缺失字段?
因为缺失字段没有进入解码流程,字段类型的方法不会被调用。把 Set 初始为 false,正好可以利用这一点表示缺失。
-
177 收藏
-
109 收藏
-
397 收藏
-
277 收藏
-
197 收藏
-
485 收藏
-
292 收藏
-
394 收藏
-
135 收藏
-
214 收藏
-
376 收藏
-
158 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习