Go 1.25 encoding/json/v2 行为变更及 v1 兼容层分析
By lemonmint views ...

Go 的 encoding/json 包 v2 是一种全新的实现,旨在改进 v1 的若干缺点(一致性不足、出人意料的行为、性能问题)。它通过 goexperiment.jsonv2 构建标签启用,属于实验性功能。
最重要的一点是,启用 v2 后,v1 将作为一个兼容层运行,在 v2 实现之上模拟 v1 的行为。这是通过 v2_v1.go 文件中的 DefaultOptionsV1() 函数实现的。也就是说,v2 提供了能够完整复现 v1 行为的选项,同时也提出了更严格、更可预测的新默认行为。
v2 的主要目标如下:
- 提高正确性和可预测性:默认采用更严格的规则(例如区分大小写、禁止重复键),以减少意外行为。
- 提升性能:重新设计解析和编码引擎,以提高效率。
- 扩展灵活性和控制权:引入详细的选项系统,使开发者能够精细地控制 JSON 处理方式。
主要语义/行为差异
以 v2_test.go 文件为中心,按项目整理了 v1 与 v2 的行为差异。
1. 字段名称匹配(区分大小写)
- v1 行为:将 JSON 对象成员反序列化到 Go 结构体字段时,**忽略大小写(case-insensitive)**进行匹配。
"FirstName"和"firstname"都会映射到FirstName字段。 - v2 行为:默认区分大小写(case-sensitive),仅映射与名称完全一致的字段。
- 变更原因:忽略大小写的匹配可能导致意外行为,并且在处理不匹配的字段时会带来性能损失。v2 采用了更清晰、更可预测的默认行为。
- 相关选项:在 v2 中,可以使用
json:"...,case:ignore"标签选项为单个字段显式启用忽略大小写,也可以全局应用json.MatchCaseInsensitiveNames(true)选项。
2. omitempty 标签选项含义的变化
- v1 行为:根据 Go 值的"空状态"省略字段。这里的"空状态"指
false、0、nil指针/接口,以及长度为 0 的数组/切片/映射/字符串。 - v2 行为:根据编码后的 JSON 值的"空状态"省略字段。即编码结果为
null、""、{}或[]时省略。 - 变更原因:v1 的定义依赖于 Go 的类型系统。v2 以 JSON 类型系统为准,从而提供更一致的行为。例如,在 v1 中
bool类型的false值会被省略,而在 v2 中false是非空的 JSON 值,因此不会被省略。v2 新增了omitzero选项,可以替代 v1 中omitempty对0或false的处理方式。 - 相关选项:若希望在 v2 中保持与 v1 相同的行为,可使用
json.OmitEmptyWithLegacyDefinition(true)选项。
3. string 标签选项行为的变化
- v1 行为:适用于数字、布尔和字符串类型字段。将对应值重新编码为 JSON 字符串中的内容(例如
int(42)->"42")。不会递归应用于复合类型(切片、映射等)内部的值。 - v2 行为:仅适用于数字类型,并且会递归应用。也就是说,
[]int等切片内部的数字也都会被编码为 JSON 字符串。 - 变更原因:
string选项的主要用途是将数字表示为字符串,以防止 64 位整数的精度丢失。v1 的行为有限且缺乏一致性。v2 专注于这一核心用途,并将其递归扩展,使其更加实用。 - 相关选项:可以使用
json.StringifyWithLegacySemantics(true)选项模拟 v1 的行为。
4. nil 切片和映射的序列化
- v1 行为:
nil切片和nil映射被序列化为null。 - v2 行为:默认情况下,
nil切片被序列化为[](空数组),nil映射被序列化为{}(空对象)。 - 变更原因:
nil是 Go 语言的实现细节,将其暴露在与语言无关的 JSON 格式中并不可取。表示空集合的[]和{}是更通用的表示方式。 - 相关选项:在 v2 中,可以通过
json.FormatNilSliceAsNull(true)或json.FormatNilMapAsNull(true)选项使其像 v1 一样序列化为null。
5. 数组反序列化
- v1 行为:反序列化到 Go 数组(
[N]T)时,即使 JSON 数组的长度与 Go 数组的长度不同也不会报错。如果长度较短,多余的位置用零值填充;如果较长,超出部分会被丢弃。 - v2 行为:JSON 数组的长度必须与 Go 数组的长度完全一致,否则会报错。
- 变更原因:在 Go 中,固定大小数组的长度通常具有重要意义。v1 的行为可能导致数据被无声丢失。v2 通过更严格的规则提高了正确性。
- 相关选项:可以使用
json.UnmarshalArrayFromAnyLength(true)选项模拟 v1 的行为。
6. time.Duration 处理
- v1 行为:
time.Duration在内部被视为int64,并编码为以纳秒为单位的 JSON 数字。 - v2 行为:使用
time.Duration.String()方法编码为"1h2m3s"等格式的 JSON 字符串。 - 变更原因:纳秒数字的可读性较差,而
time.Duration的标准字符串表示更加实用。 - 相关选项:可以通过
json:",format:nano"标签选项或json.FormatTimeWithLegacySemantics(true)选项使用 v1 的行为。
7. 无效 UTF-8 的处理
- v1 行为:在序列化/反序列化时,如果字符串中包含无效的 UTF-8 字节,会静默替换为 Unicode 替换字符(
\uFFFD)。 - v2 行为:默认情况下,遇到无效 UTF-8 时返回错误。
- 变更原因:防止数据被无声损坏,并遵循更严格的 JSON 标准(RFC 7493)。
- 相关选项:可以使用
jsontext.AllowInvalidUTF8(true)选项模拟 v1 的行为。
8. 重复的对象成员名称处理
- v1 行为:允许 JSON 对象中出现同名的重复成员。后出现的值会覆盖先前的值。
- v2 行为:默认情况下,如果存在重复的成员名称,则返回错误。
- 变更原因:RFC 8259 标准未定义重复名称的行为,因此不同实现可能表现不同。这可能成为安全漏洞的根源。v2 明确拒绝此类输入,以提高正确性和安全性。
- 相关选项:可以使用
jsontext.AllowDuplicateNames(true)选项模拟 v1 的行为。
实现与架构差异
- v1:主要依赖
decode.go中的decodeState以及scanner.go中手写的状态机(state machine)。这是一种解析逻辑与语义分析紧密耦合的单体结构。 - v2:架构更加模块化。
encoding/json/jsontext:提供底层、高性能的 JSON 词法分析器(Decoder)和编码器(Encoder)。该包仅关注 JSON 的语法层面。encoding/json/v2:基于jsontext,负责处理 Go 类型与 JSON 值之间的语义转换。- 通过这种分离,语法分析与语义分析彼此独立,代码的清晰度和性能均得到提升。
v2 的新 API 与功能
v2 通过 json.Options 系统提供了非常灵活的控制功能。
json.Options:一组用于改变序列化/反序列化行为的选项的集合。json.JoinOptions(...):将多个选项合并为一个。WithMarshalers/WithUnmarshalers:无需实现Marshaler/Unmarshaler接口,即可为特定类型注入序列化/反序列化逻辑的强大功能。这在处理外部包中的类型时尤其有用。- 新增选项:
RejectUnknownMembers、Deterministic(false)、FormatNilSliceAsNull等,使得许多 v1 无法实现的行为控制成为可能。
结论
encoding/json v2 基于 v1 的经验,在正确性、性能和灵活性方面都有显著提升,是一种现代化的实现。虽然默认行为变得更加严格,但通过完善的 Options 系统,可以完整支持 v1 的所有行为,因此可以在保持与现有代码兼容的同时,逐步引入 v2 的优势。
- 对于新项目,建议默认使用 v2。
- 对于现有项目,可以继续直接使用
jsonv1,或者在迁移到jsonv2的过程中,通过DefaultOptionsV1()逐步引入 v2 的严格行为。