当前位置:首页 > 文章列表 > Golang > Go问答 > Go jsonnull 如何限定字段范围

Go jsonnull 如何限定字段范围

来源:17golang原创 2026-09-13 10:38:25 0浏览 收藏

如果一个 Go 接口只需要“有值或没值”,普通指针已经够用;但 PATCH 更新常常还要区分“不修改”“明确清空”和“设置为空数组”。这时可以把 jsonnull 作为一个小型三态包装器,只放在确实需要这三种语义的请求字段上。它的关键不是让所有模型都变复杂,而是把字段范围限定在更新边界。

本文示例使用标准库 encoding/json 的自定义解码接口来实现这个边界。Go 官方文档说明,JSON 映射到 Go 值时,null 对非指针普通值不会自动留下“字段出现过”的信息,因此需要由包装类型自己记录。

先划清 jsonnull 的适用字段

建议先问一个问题:服务端是否必须知道客户端有没有提交这个字段?答案为“否”的响应 DTO、查询结果和只读配置,不需要使用 jsonnull。它们可以使用普通值、指针或 sql.Null* 等更符合自身边界的类型。

答案为“是”的 PATCH DTO 才适合使用它。例如个人资料更新中,nickname 可能被清空,tags 还要区分 null[]

jsonnull 字段范围与三态语义的静态关系图
图1:字段范围示意图,只有 PATCH DTO 的目标字段进入 jsonnull 三态边界。

这样做的收益是边界清楚:接收层负责表达请求意图,业务层负责把意图翻译成更新动作,持久化层不必猜测一个零值到底代表什么。

用三态值区分缺失、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 输入协议泄漏到存储模型,也容易让查询和响应出现不必要的三态判断。

PATCH DTO 中 nickname 和 tags 的三态边界关系图
图2:更新边界示意图,nickname 与 tags 分别把有效值、null、空数组映射到明确动作。

用表格测试边界而不是猜 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 能替代所有指针吗?不能。只在“字段出现与否”会改变业务动作时使用;普通可选响应字段用指针通常更简单。

为什么不只用 omitemptyomitempty 主要影响编码时是否省略空值,不能在解码后告诉你字段是否出现在请求中,也不能单独表达 null 与空数组的业务含义。

应该把 Present 和 Valid 存进数据库吗?通常不应该。它们是请求期间的意图标记,业务层消费后只持久化真正的领域值。

总结

  • jsonnull 的核心价值是保存“出现过”这一信息。
  • 只把它放在 PATCH 等部分更新 DTO 的字段上。
  • 对集合字段明确约定:null 是清空或移除,空数组是设置为空集合。

官方参考:https://pkg.go.dev/encoding/json

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Kubernetes v1.37 etcd RangeStream 大列表如何降低内存Kubernetes v1.37 etcd RangeStream 大列表如何降低内存
上一篇
Kubernetes v1.37 etcd RangeStream 大列表如何降低内存
WeakMap 生命周期怎么配置或排查
下一篇
WeakMap 生命周期怎么配置或排查
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    111次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    31次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    49次使用
  • AGI-Eval大模型评测平台:权威榜单、数据集与人机协同评测方案
    AGI-Eval
    AGI-Eval是由上海交大等高校联合发布的大模型评测社区,提供公正透明的LLM能力榜单、多领域评测集及Data Studio数据服务,助力AI模型性能评估与NLP科研开发。
    30次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    265次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码