Go 1.27 发布稳定的 encoding/json/v2 与 jsontext 新 API
来源标题:A new Go API for JSON
Go 1.27 正式提供稳定的 encoding/json/v2 和 encoding/json/jsontext 包,取代此前的实验性 JSON API。新包旨在解决旧版 encoding/json 长期存在的问题,此前已根据反馈进行改进。Go 标准库对 JSON 的支持可追溯至 Go 1.0 之前。
以下正文由 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 接口的类型也无法使用这些选项,而且没有办法把选项沿着调用栈向下传递。例如,在调用自定义的 UnmarshalJSON 方法时,Decoder.DisallowUnknownFields 选项就失去了作用。 Compact、Indent 和 HTMLEscape 函数写入的是 bytes.Buffer,而不是 []byte 或 io.Writer 这样更灵活的目标。这限制了这些函数的可用性。
暂且不谈内部实现细节,公开 API 本身就把这个包限制在了某些性能瓶颈上:
MarshalJSON:MarshalJSON 接口方法迫使实现必须分配它返回的 []byte。此外,其语义要求 encoding/json 验证结果是合法的 JSON,并且还要重新格式化结果以匹配指定的缩进。 UnmarshalJSON:UnmarshalJSON 接口方法要求提供一个完整的 JSON 值(不能有任何尾随数据)。这迫使 encoding/json 必须先完整解析出要 unmarshal 的 JSON 值,以确定它在何处结束,然后才能调用 UnmarshalJSON。之后,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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。