Go jsonnull 如何限定字段范围
如果一个 Go 接口只需要“有值或没值”,普通指针已经够用;但 PATCH 更新常常还要区分“不修改”“明确清空”和“设置为空数组”。这时可以把 jsonnull 作为一个小型三态包装器,只放在确实需要这三种语义的请求字段上。它的关键不是让所有模型都变复杂,而是把字段范围限定在更新边界。
本文示例使用标准库 encoding/json 的自定义解码接口来实现这个边界。Go 官方文档说明,JSON 映射到 Go 值时,null 对非指针普通值不会自动留下“字段出现过”的信息,因此需要由包装类型自己记录。
先划清 jsonnull 的适用字段
建议先问一个问题:服务端是否必须知道客户端有没有提交这个字段?答案为“否”的响应 DTO、查询结果和只读配置,不需要使用 jsonnull。它们可以使用普通值、指针或 sql.Null* 等更符合自身边界的类型。
答案为“是”的 PATCH DTO 才适合使用它。例如个人资料更新中,nickname 可能被清空,tags 还要区分 null 和 []:

这样做的收益是边界清楚:接收层负责表达请求意图,业务层负责把意图翻译成更新动作,持久化层不必猜测一个零值到底代表什么。
用三态值区分缺失、null 和空数组
一个可读的泛型类型可以包含三个状态:Present=false 表示字段缺失;Present=true、Valid=false 表示显式 null;两个布尔值都为真时,Value 才是有效值。对于 []string,有效值可以是空切片,因此它和 null 不是一回事。
// JSONNull 只描述请求字段的三态状态,不承担数据库持久化职责。
type JSONNull[T any] struct {
Value T
Valid bool // true 表示字段不是 JSON null
Present bool // true 表示请求中出现了该字段
}
// NewValue 构造一个已提交的有效值,包括空切片。
func NewValue[T any](v T) JSONNull[T] {
return JSONNull[T]{Value: v, Valid: true, Present: true}
}
// NewNull 构造一个需要清空目标字段的显式 null。
func NewNull[T any]() JSONNull[T] {
return JSONNull[T]{Present: true}
}
这里的 jsonnull 是应用内命名,不是 Go 标准库中的独立类型。命名可以按项目习惯调整,但三态字段必须有稳定、可读的判断方法,不能依赖调用方直接猜布尔字段组合。
把字段范围收口到 PATCH 更新层
解码时要使用指针接收者,因为只有它能修改包装器中的状态。对 null 只记录出现,不把零值误当成有效值;对其他 JSON 值再解码到 Value。下面的代码只展示边界逻辑,调用方仍应处理返回错误。
// UnmarshalJSON 记录字段是否出现,并保留 null 与空数组的差别。
func (j *JSONNull[T]) UnmarshalJSON(data []byte) error {
j.Present = true
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
j.Valid = false // 显式 null:业务层通常解释为清空
var zero T
j.Value = zero
return nil
}
if err := json.Unmarshal(data, &j.Value); err != nil {
j.Valid = false // 解码失败时不产生可用值
return err
}
j.Valid = true
return nil
}
// PatchProfile 只在部分更新入口使用三态字段。
type PatchProfile struct {
Nickname JSONNull[string] `json:"nickname"`
Tags JSONNull[[]string] `json:"tags"`
}
// ApplyProfilePatch 把三态请求翻译为明确的业务动作。
func ApplyProfilePatch(p PatchProfile, dst *Profile) {
if p.Nickname.Present {
if p.Nickname.Valid {
dst.Nickname = p.Nickname.Value // 有效字符串,包括空字符串
} else {
dst.Nickname = "" // null 表示清空
}
}
if p.Tags.Present {
if p.Tags.Valid {
dst.Tags = append([]string(nil), p.Tags.Value...) // [] 表示设置为空集合
} else {
dst.Tags = nil // null 表示移除集合值
}
}
}
字段范围就在 PatchProfile 这一层收口。不要为了“统一”把数据库实体的所有可空列都替换成 JSONNull;那会把 HTTP 输入协议泄漏到存储模型,也容易让查询和响应出现不必要的三态判断。

用表格测试边界而不是猜 tag
最小测试只需覆盖字段缺失、显式 null、普通值和空数组。注意:encoding/json 的字段标签只决定名称匹配,不会替你记录字段是否出现;这个信息必须由 UnmarshalJSON 保存。
// 这组表驱动用例验证请求意图,而不是只比较最终零值。
tests := []struct {
name string
body string
p PatchProfile
wantPresent bool
wantValid bool
}{
{"缺失", `{}`, PatchProfile{}, false, false},
{"null", `{"tags":null}`, PatchProfile{}, true, false},
{"空数组", `{"tags":[]}`, PatchProfile{}, true, true},
}
for _, tt := range tests {
var got PatchProfile
err := json.Unmarshal([]byte(tt.body), &got) // 注释:解码失败应让测试直接失败
if err != nil {
t.Fatalf("%s: %v", tt.name, err)
}
if got.Tags.Present != tt.wantPresent || got.Tags.Valid != tt.wantValid {
t.Fatalf("%s: got present=%v valid=%v", tt.name, got.Tags.Present, got.Tags.Valid)
}
}
实际项目还应补上错误 JSON、数组元素类型错误以及更新后持久化失败的用例。最终判断标准不是“用了哪个包”,而是每个字段的协议是否明确:缺失不更新,null 清空,空数组设置为空集合。
常见问题
jsonnull 能替代所有指针吗?不能。只在“字段出现与否”会改变业务动作时使用;普通可选响应字段用指针通常更简单。
为什么不只用 omitempty?omitempty 主要影响编码时是否省略空值,不能在解码后告诉你字段是否出现在请求中,也不能单独表达 null 与空数组的业务含义。
应该把 Present 和 Valid 存进数据库吗?通常不应该。它们是请求期间的意图标记,业务层消费后只持久化真正的领域值。
总结
jsonnull的核心价值是保存“出现过”这一信息。- 只把它放在 PATCH 等部分更新 DTO 的字段上。
- 对集合字段明确约定:null 是清空或移除,空数组是设置为空集合。
官方参考:https://pkg.go.dev/encoding/json
Kubernetes v1.37 etcd RangeStream 大列表如何降低内存
- 上一篇
- Kubernetes v1.37 etcd RangeStream 大列表如何降低内存
- 下一篇
- WeakMap 生命周期怎么配置或排查
-
- Golang · Go问答 | 1小时前 |
- Go debug/pe 文件信息缺失时如何定位构建参数
- 203浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 1.25 testing/synctest 如何让并发测试不再依赖真实等待
- 413浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go 外部测试包隔离集成测试依赖循环的实现方案
- 293浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go go env GOEXPERIMENT 怎样查看当前编译实验开关
- 144浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go test -run 正则为何没有匹配目标用例
- 242浏览 收藏
-
- Golang · Go问答 | 4小时前 | 测试 · go · Go 模糊测试 go test fuzz
- Go fuzz 测试如何保存和复现崩溃输入
- 138浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go test -json 怎样把测试事件导出给日志系统
- 152浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go pprof 标签没有出现在 profile 中怎么办
- 301浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go testing/synctest bubble 里时间为什么自动推进
- 127浏览 收藏
-
- Golang · Go问答 | 4小时前 | 单元测试 · go · Go testing.T.Cleanup testing.Parallel
- Go testing.Parallel 与 Cleanup 的执行顺序如何理解
- 166浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go race 检测器遇到 cgo 代码如何判断范围
- 381浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 764次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 760次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 842次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 813次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 641次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go generate 未执行指令的注释与目录检查
- 2026-10-28 501浏览
-
- Go RWMutex 写锁等待过久的竞争分析
- 2026-10-28 501浏览
-
- Go os.File.ReadAt 为什么到文件末尾仍返回部分数据
- 2026-10-26 501浏览

