Go 1.27 发布新的 JSON API,encoding/json/v2 稳定可用
来源标题:A New Go API for JSON
Go 1.27 正式提供稳定的 encoding/json/v2 和 encoding/json/jsontext 包,这是对原有 encoding/json 的改进版本。新 API 经过一年实验和反馈打磨后转正,旨在解决旧包长期存在的问题。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Michael Pratt、Joe Tsai、Daniel Martí、Johan Brandhorst-Satzkorn、Roger Peppe、Chris Hines 和 Damien Neil 2026年11月4日
JavaScript Object Notation(JSON)是一种简单的数据交换格式,是互联网上最流行的数据格式,也被 Go 程序广泛读写。早在 Go 1.0 之前,Go 标准库就已经支持 JSON。我们第一次撰文介绍 Go 对 JSON 的支持,已经是 15 多年前的事了!
随着时间的推移,软件包会随着用户需求而演进,encoding/json 也不例外。去年,我们介绍了一个新的实验性 JSON API,旨在带来人们期待已久的改进和修复。经过进一步的反馈和打磨,新的 encoding/json/v2 和 encoding/json/jsontext 包已经稳定,并在 Go 1.27 中正式可用!本文是介绍该实验的文章的更新版,描述了 encoding/json 存在的问题,并对新包做了介绍。
总体而言,encoding/json 经受住了时间的考验。用某种默认的 JSON 表示来对任意 Go 类型进行 marshal 和 unmarshal,再加上自定义表示的能力,这一思路已被证明极具灵活性。然而,自它推出以来的这些年里,各类用户发现了许多不足之处。
encoding/json 存在各种行为缺陷:
对 JSON 语法的处理不够精确:多年来,JSON 的标准化程度不断提高,以便程序之间能够正确通信。总体而言,解码器在拒绝歧义输入方面变得更加严格,以减少两种实现对某个特定 JSON 值产生不同(但都成功)的解释的可能性。encoding/json 目前接受无效的 UTF-8,而最新的互联网标准(RFC 8259)要求使用有效的 UTF-8。默认行为应当在遇到无效 UTF-8 时报错,而不是引入静默的数据损坏,后者可能在下游引发问题。
encoding/json 目前接受成员名重复的对象。RFC 8259 没有规定如何处理重复的名称,因此实现可以自由选择任意一个值、合并这些值、丢弃这些值或报错。重复名称的存在会导致某个 JSON 值缺乏普遍公认的含义。这可能被攻击者在安全应用中所利用,而且过去确实被利用过(如 CVE-2017-12635)。默认行为应当以安全为先,拒绝重复的名称。
泄露 slice 和 map 的 nil 性质:JSON 常被用于与其他程序通信,而这些程序使用的 JSON 实现不允许将 null unmarshal 到预期为 JSON 数组或对象的数据类型中。由于 encoding/json 会将 nil slice 或 map marshal 为 JSON null,这可能导致其他实现在 unmarshal 时出错。一项调查表明,大多数 Go 用户希望默认将 nil slice 和 map marshal 为空 JSON 数组或对象。
大小写不敏感的 unmarshal:在 unmarshal 时,JSON 对象成员名会通过大小写不敏感的匹配解析到 Go 结构体字段名。这是一个令人意外的默认行为,是潜在的安全漏洞,也是性能上的限制。
方法调用的不一致性:由于某个实现细节,声明在指针接收者上的 MarshalJSON 方法会被 encoding/json 不一致地调用。虽然这被视为一个 bug,但由于太多应用依赖当前行为,无法修复。
encoding/json 的 API 可能很棘手或限制性强:
很难正确地从 io.Reader 进行 unmarshal。用户常写 json.NewDecoder(r).Decode(v),但这无法拒绝输入末尾的多余内容。
选项可以设置在 Encoder 和 Decoder 类型上,但无法与 Marshal 和 Unmarshal 函数一起使用。同样,实现了 Marshaler 和 Unmarshaler 接口的类型也无法使用这些选项,而且没有办法沿调用栈向下传递选项。例如,Decoder.DisallowUnknownFields 选项在调用自定义的 UnmarshalJSON 方法时会失效。
Compact、Indent 和 HTMLEscape 函数写入 bytes.Buffer,而不是像 []byte 或 io.Writer 这样更灵活的目标。这限制了这些函数的可用性。
暂且不谈内部实现细节,公开 API 也使其注定存在某些性能限制:
MarshalJSON:MarshalJSON 接口方法强制实现者分配返回的 []byte。此外,其语义要求 encoding/json 验证结果是否为有效 JSON,并重新格式化以匹配指定的缩进。
UnmarshalJSON:UnmarshalJSON 接口方法要求提供完整的 JSON 值(不带任何尾随数据)。这迫使 encoding/json 在调用 UnmarshalJSON 之前,先完整解析待 unmarshal 的 JSON 值以确定其结束位置。之后,UnmarshalJSON 方法本身又必须再次解析所提供的 JSON 值。
缺乏流式处理:尽管 Encoder 和 Decoder 类型基于 io.Writer 或 io.Reader 工作,它们却将整个 JSON 值缓冲在内存中。用于读取单个 token 的 Decoder.Token 方法分配开销很大,而且没有对应的写入 token 的 API。此外,如果 MarshalJSON 或 UnmarshalJSON 方法的实现递归调用 Marshal 或 Unmarshal 函数,性能就会退化为平方级。
引入一个新的、不兼容的包主版本是一个需要慎重考虑的决定。如果可能,我们应当尝试修复现有的包。虽然添加新功能相对容易,但修改现有功能却很困难。不幸的是,这些问题都是现有 API 的固有后果,使得它们在 Go 1 兼容性承诺的范围内几乎不可能修复。原则上我们可以声明单独的名称,例如 MarshalV2 或 UnmarshalV2,但这无异于在同一个包内创建一个平行的命名空间。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。