单一来源
当前只有一个可用来源,尚未完成多源交叉核验Go语言新增JSON API接口
Go 博客
JSON 的新 Go API
引言
JavaScript Object Notation (JSON) 是一种简单的数据交换格式,是互联网上最流行的数据格式,被 Go 程序广泛读写。自 Go 1.0 之前很久,Go 就在标准库中包含了 JSON 支持。早在 15 多年前,我们就曾写过关于 Go 中 JSON 支持的文章!
随着时间的推移,包会随着用户的需求而演变,encoding/json 也不例外。去年,我们曾写过一篇关于新的 JSON 实验性 API 的文章,旨在带来期待已久的改进和修复。
在获得更多反馈和改进后,新的 encoding/json/v2 和 encoding/json/jsontext 包在 Go 1.27 中已稳定并正式发布!本文是对那篇介绍实验的文章的更新版本,描述了 encoding/json 的问题,并介绍了新包。
encoding/json 的问题
总体而言,encoding/json 表现良好。将任意 Go 类型与 JSON 中的某种默认表示形式进行编组和解组,并结合自定义表示形式的能力,已被证明具有高度的灵活性。然而,自其引入以来的这些年里,各种用户指出了许多不足之处。
行为缺陷
encoding/json 中存在各种行为缺陷:
对 JSON 语法的处理不精确:多年来,JSON 的标准化程度不断提高,以便程序能够正确通信。通常,解码器对拒绝歧义输入变得更加严格,以减少两个实现对特定 JSON 值产生不同(成功)解释的可能性。
encoding/json 目前接受无效的 UTF-8,而最新的互联网标准 (RFC 8259) 要求有效的 UTF-8。默认行为应在存在无效 UTF-8 时报告错误,而不是引入静默的数据损坏,这可能会导致下游出现问题。
encoding/json 目前接受具有重复成员名称的对象。RFC 8259 未指定如何处理重复名称,因此实现可以自由选择任意值、合并值、丢弃值或报告错误。重复名称的存在导致 JSON 值没有普遍公认的含义。这可能会被攻击者在安全应用程序中利用,并且以前曾被利用过(如 CVE-2017-12635)。默认行为应倾向于安全,并拒绝重复名称。
泄漏切片和映射的 nilness:JSON 常用于与使用 JSON 实现的程序通信,这些实现不允许将 null 解组到预期为 JSON 数组或对象的预期数据类型中。由于 encoding/json 将 nil 切片或映射编组为 JSON null,这可能在由其他实现解组时导致错误。
一项调查显示,大多数 Go 用户希望默认情况下 nil 切片和映射被编组为空的 JSON 数组或对象。
不区分大小写的反序列化:在反序列化时,JSON 对象成员名称通过不区分大小写的匹配解析为 Go 结构体字段名称。这是一个令人惊讶的默认行为,可能存在安全漏洞,并且是一种性能限制。
方法调用不一致:由于实现细节,在指针接收器上声明的 MarshalJSON 方法会被 encoding/json 不一致地调用。虽然这被视为一个 bug,但无法修复,因为太多应用程序依赖于当前的行为。
API 缺陷
encoding/json 的 API 可能比较棘手或受限:
很难从 io.Reader 正确反序列化。用户经常编写 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 完全解析要反序列化的 JSON 值,以确定其结束位置,然后才能调用 UnmarshalJSON。之后,UnmarshalJSON 方法本身必须再次解析提供的 JSON 值。
缺乏流式处理:尽管 Encoder 和 Decoder 类型操作 io.Writer 或 io.Reader,但它们在内存中缓冲整个 JSON 值。用于读取单个令牌的 Decoder.Token 方法分配开销很大,并且没有相应的 API 用于写入令牌。
此外,如果 MarshalJSON 或 UnmarshalJSON 方法的实现递归调用 Marshal 或 Unmarshal 函数,那么性能将变为二次方。
尝试直接修复 encoding/json
引入一个不兼容的新大版本包是一个重大的考量。如果可能,我们应该尝试修复现有的包。
虽然添加新功能相对容易,但更改现有功能却很困难。不幸的是,这些问题是现有 API 的固有后果,使得在 Go 1 兼容性承诺范围内实际上无法修复。
我们原则上可以声明单独的名称,例如 MarshalV2 或 UnmarshalV2,但这相当于在同一包内创建了一个平行的命名空间。这引导我们走向 encoding/json/v2(以下简称 v2),与 encoding/json(以下简称 v1)不同,我们可以在 v2 的独立命名空间内进行这些更改。
基于 encoding/json/jsontext
在讨论 v2 API 之前,我们首先介绍 encoding/json/jsontext 包,它为改进 Go 中的 JSON 奠定了基础。
Go 中的 JSON 序列化可以分解为两个主要组成部分:
· 语法功能,即基于 JSON 语法进行处理的功能,以及
· 语义功能,即定义 JSON 值与 Go 值之间关系的功能。
我们使用“编码”和“解码”来描述语法功能,使用“编组”和“解组”来描述语义功能。我们的目标是明确区分仅涉及编码的功能与涉及编组的功能。
下图概述了这种分离。紫色块代表类型,蓝色块代表函数或方法。箭头的方向大致代表数据流。图的下半部分由 jsontext 包实现,包含仅涉及语法的功能;而上半部分由 json/v2 包实现,包含为下半部分处理的语法数据赋予语义的功能。
jsontext 的基本 API 如下:
package jsontext
type Encoder struct { ... } func NewEncoder(io.Writer, ...Options) *Encoder func (*Encoder) WriteValue(Value) error func (*Encoder) WriteToken(Token) error
type Decoder struct { ... } func NewDecoder(io.Reader, ...Options) *Decoder func (*Decoder) ReadValue() (Value, error) func (*Decoder) ReadToken() (Token, error)
type Kind byte type Value []byte func (Value) Kind() Kind type Token struct { ... } func (Token) Kind() Kind
jsontext 包提供了与 JSON 交互的功能,但
Evidence
来源与证据
当前仅有一个可用来源,尚未完成多源交叉核验。
- 发布时间
- 内容处理
- AI 翻译 / 摘要,需结合原文核对
相关资讯
AI 翻译、摘要和解读仅用于辅助判断;涉及重要决策时,请回到上方来源核实。