GoSuda

Go 1.25 encoding/json v1 与 v2 比较

By lemonmint
views ...

Go语言的encoding/json包v2版本是对现有v1版本中诸多不足(例如:缺乏一致性、意外行为、性能问题)进行改进后的全新实现。这是一个实验性功能,可以通过goexperiment.jsonv2构建标签来启用。

最重要的一点是,当v2被激活时,v1将作为兼容层运行,通过v2实现来模拟v1的行为。这通过v2_v1.go文件中的DefaultOptionsV1()函数实现。换言之,v2提供了能够完美重现v1行为的选项,同时引入了更严格且可预测的新默认行为。

v2的主要目标如下:

  1. 提高准确性和可预测性:默认应用更严格的规则(例如:区分大小写、禁止重复键),以减少意外行为。
  2. 改善性能:重新设计了解析和编码引擎,提高了效率。
  3. 扩大灵活性和控制权:引入了详细的Options系统,使开发者能够精细控制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值的“空状态”省略字段。此处的“空状态”指false0nil指针/接口、长度为0的数组/切片/映射/字符串。
  • v2行为:根据编码后的JSON值的“空状态”省略字段。即,如果编码为null""{}[],则省略。
  • 变更原因:v1的定义依赖于Go的类型系统。v2以JSON类型系统为基础,提供了更一致的行为。例如,在v1中,bool类型的false值会被省略,但在v2中,false不是空的JSON值,因此不会被省略。v2中增加了omitzero选项,可以替代v1中omitempty应用于0false的行为。
  • 相关选项:在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切片和映射的Marshal

  • v1行为nil切片和nil映射被Marshal为null
  • v2行为:默认情况下,nil切片被Marshal为[](空数组),nil映射被Marshal为{}(空对象)。
  • 变更原因nil是Go语言的实现细节,将其暴露给与语言无关的JSON格式是不理想的。表示空集合的[]{}是更通用的表达方式。
  • 相关选项:在v2中,可以通过json.FormatNilSliceAsNull(true)json.FormatNilMapAsNull(true)选项,像v1一样将其Marshal为null

5. 数组Unmarshal

  • v1行为:当Unmarshal到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行为:在Marshal/Unmarshal时,如果字符串中存在无效的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中的decodeStatescanner.go中手动编写的状态机(state machine)。这是一种解析逻辑和语义分析紧密耦合的单体结构。
  • v2:架构更加模块化。
    • encoding/json/jsontext:提供低级别、高性能的JSON分词器(Decoder)和编码器(Encoder)。此包仅关注JSON的语法(syntax)方面。
    • encoding/json/v2:基于jsontext处理Go类型和JSON值之间的语义(semantic)转换。
    • 这种分离使得语法分析和语义分析得以分离开来,从而提高了代码的清晰度和性能。

v2的新API和功能

v2通过json.Options系统提供了高度灵活的控制功能。

  • json.Options:一组用于改变Marshal/Unmarshal行为的选项。
  • json.JoinOptions(...):将多个选项合并为一个。
  • WithMarshalers / WithUnmarshalers:一项强大的功能,无需实现Marshaler/Unmarshaler接口,即可注入特定类型的序列化/反序列化逻辑。这在处理外部包的类型时特别有用。
  • 新选项RejectUnknownMembersDeterministic(false)FormatNilSliceAsNull等,提供了v1中无法实现的各种行为控制。

结论

encoding/json v2在v1的经验基础上,显著提高了准确性、性能和灵活性,是一种现代化的实现。尽管其默认行为更为严格,但通过精密的Options系统,它能够完美支持v1的所有行为,从而在保持与现有代码兼容性的同时,逐步引入v2的优势。

  • 新项目建议默认使用v2。
  • 现有项目可以继续使用jsonv1,或者通过DefaultOptionsV1()逐步迁移到jsonv2,并逐渐引入v2的严格行为策略。