当前位置:首页 > 文章列表 > Golang > Go教程 > Go encoding/json 怎样区分缺失字段和显式 null完成一个可复用的最小示例

Go encoding/json 怎样区分缺失字段和显式 null完成一个可复用的最小示例

来源:17golang原创 2026-11-07 03:30:20 0浏览 收藏

我在做一个“部分更新配置”的接口时,最容易被忽略的不是 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 意味着清空、具体值意味着覆盖。

Optional 状态封装把 JSON 字段映射到 Present、Null 和 Value 的静态关系说明图
图1:结构说明图,查看请求字段、Optional 封装和三种状态之间的静态关系;这不是运行截图。

动态字段用 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 字节判断动态字段状态的关系说明图
图2:结构说明图,查看动态字段从 map 到 RawMessage 再到 missing、null、value 分支的关系;这不是运行截图。

复用对象时的反例和选择清单

第一个反例是把同一个 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 影响的是编码输出,不是解码时的字段存在性判断。它不能让普通结构体在解码后自动知道某个键是否出现过。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go unique.Handle 怎样比较唯一句柄而不复制字符串处理空输入并保留错误边界Go unique.Handle 怎样比较唯一句柄而不复制字符串处理空输入并保留错误边界
上一篇
Go unique.Handle 怎样比较唯一句柄而不复制字符串处理空输入并保留错误边界
Go encoding/json 怎样区分缺失字段和显式 null在批处理代码中减少额外分配
下一篇
Go encoding/json 怎样区分缺失字段和显式 null在批处理代码中减少额外分配
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    920次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    925次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1024次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1003次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    831次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码