Go encoding/json 怎样区分缺失字段和显式 null完成一个可复用的最小示例
来源:17golang原创
时间:2026-11-07 03:30:20 260浏览 收藏
我在做一个“部分更新配置”的接口时,最容易被忽略的不是 JSON 语法,而是字段状态:客户端不传字段,通常表示保持原值;客户端传入 null,可能表示清空;客户端传入具体值,则表示覆盖。如果直接把请求解码到普通 Go 结构体,这三种意思很容易被压成同一个零值。
官方文档:https://pkg.go.dev/encoding/json
这篇保留一个足够小、可以复用的模式:固定协议使用带状态的 Optional[T],动态协议使用 map[string]json.RawMessage。前者适合业务更新对象,后者适合字段不固定的 JSON。
先看普通结构体为什么丢失状态
标准 encoding/json 在解码结构体时,会按 JSON 键匹配字段;输入中没有出现的键不会主动替你写入。对于指针字段,JSON null 会把指针设为 nil,于是“没出现”和“明确传 null”都可能表现为同一个结果。
package main
import (
"encoding/json"
"fmt"
)
type Patch struct {
// 普通指针只能表达当前值,不能可靠表达字段是否出现过。
Name *string `json:"name"`
}
func main() {
for _, input := range []string{`{}`, `{"name":null}`, `{"name":"Go"}`} {
var patch Patch
if err := json.Unmarshal([]byte(input), &patch); err != nil {
// 输入不是合法 JSON 时,先返回解析错误,不继续处理更新。
fmt.Println("decode error:", err)
continue
}
fmt.Printf("%s => name is nil: %v\n", input, patch.Name == nil)
}
}
这里前两种输入都会得到 patch.Name == nil。如果目标是“把 null 当成清空指令”,这个信息已经无法从结构体字段本身恢复。更隐蔽的是:如果复用同一个目标对象,缺失字段还会保留它原来的值,所以每次解码前都要明确是否使用新对象。
把三种含义收进 Optional 状态封装
我更常用的做法是给可选字段增加两个状态位:Present 表示键在 JSON 中出现过,Null 表示它的值是显式 null。只有既出现又非 null 时,Value 才有业务意义。
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Optional[T any] struct {
// Present 区分“请求没有这个键”和“请求明确传了一个值”。
Present bool
// Null 区分“出现了 null”和“出现了可解码的具体值”。
Null bool
Value T
}
func (o *Optional[T]) UnmarshalJSON(data []byte) error {
// UnmarshalJSON 会在字段出现时调用,包括输入为 null 的情况。
o.Present = true
o.Null = bytes.Equal(bytes.TrimSpace(data), []byte("null"))
if o.Null {
var zero T
o.Value = zero
return nil
}
// 具体值解码失败时返回原始错误,避免把坏输入误当成缺失字段。
return json.Unmarshal(data, &o.Value)
}
type Patch struct {
Name Optional[string] `json:"name"`
}
func main() {
for _, input := range []string{`{}`, `{"name":null}`, `{"name":"Go"}`} {
var patch Patch
if err := json.Unmarshal([]byte(input), &patch); err != nil {
fmt.Println("decode error:", err)
continue
}
fmt.Printf("%s => present=%v null=%v value=%q\n",
input, patch.Name.Present, patch.Name.Null, patch.Name.Value)
}
}
结果可以按三态读取:Present=false 是缺失;Present=true, Null=true 是显式 null;两个状态都为真且 Null=false 时读取 Value。这个模式的关键不是泛型,而是 UnmarshalJSON 只负责记录“字段出现过”,业务层再决定缺失意味着保持、null 意味着清空、具体值意味着覆盖。

动态字段用 RawMessage 检查键是否存在
如果字段集合不是固定的,或者你只想在某个局部判断状态,可以先解码成 map[string]json.RawMessage。map 的第二个返回值负责判断键是否存在,RawMessage 的字节内容再负责判断是否为 null。
func classify(input []byte, key string) (string, error) {
var fields map[string]json.RawMessage
if err := json.Unmarshal(input, &fields); err != nil {
// 语法错误直接交给调用方,避免把错误输入当成字段缺失。
return "invalid-json", err
}
raw, ok := fields[key]
if !ok {
// 键不存在,表示调用方没有提出这项更新。
return "missing", nil
}
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
// 键存在但值为 null,通常对应清空动作。
return "null", nil
}
return "value", nil
}
这个方式不会自动把具体值解码成目标类型,因此适合先分流、后按字段策略解码的入口。需要注意的是,空字符串、数字零、空数组都不是 null;它们属于具体值,是否视为“清空”要由协议约定。
![map[string]json.RawMessage 通过键存在性和 null 字节判断动态字段状态的关系说明图](/uploads/20261107/1793993419-b5bd53dae9-0ce22b2959-raw-message-branch.webp)
复用对象时的反例和选择清单
第一个反例是把同一个 Patch 连续用于多个请求:第二次输入缺失 name 时,旧的 Present 和 Value 可能仍在。最简单的修复是每次请求创建新目标;如果必须复用,就在解码前完整清零,而不是只清理 Value。
第二个反例是只使用 *string 或 interface{},然后把 nil 直接解释成“清空”。这会把客户端没有提出变更的请求误判成清空请求。固定字段优先选 Optional[T],需要动态键或延迟解码时选 map[string]json.RawMessage。
最后可以用这张小清单做判断:要区分缺失和 null,就必须保留“键是否出现”的状态;要区分 null 和具体值,就必须检查原始 JSON 或在自定义解码器里设置标记;要避免旧值污染,就不要把缺失字段当成“自动清零”。
常见问题
只用 json.RawMessage 字段可以吗?
可以。结构体中的非指针 json.RawMessage 通常能用 nil 表示缺失、用字节串 null 表示显式 null,但要注意目标对象复用时的旧值问题;固定业务协议仍建议用带状态封装表达意图。
为什么不直接给字段加 omitempty?
omitempty 影响的是编码输出,不是解码时的字段存在性判断。它不能让普通结构体在解码后自动知道某个键是否出现过。
-
278 收藏
-
483 收藏
-
291 收藏
-
195 收藏
-
412 收藏
-
430 收藏
-
439 收藏
-
462 收藏
-
318 收藏
-
203 收藏
-
295 收藏
-
340 收藏
-
466 收藏
-
364 收藏
-
413 收藏
-
107 收藏
-
190 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习