跳到正文
认知
返回认知精选
认知精选科技大约 8 小时前1
单一来源当前只有一个可用来源,尚未完成多源交叉核验

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

来源与证据

单一来源 · 1 个信源

当前仅有一个可用来源,尚未完成多源交叉核验。

发布时间
内容处理
AI 翻译 / 摘要,需结合原文核对
内容有误或来源失效?告诉我们

每天早上收到这 5 条

免注册,填邮箱即可;每封邮件都能一键退订。

提交即表示同意按隐私说明保存你的邮箱,只用于发送简报。

或用 RSS 订阅

AI 翻译、摘要和解读仅用于辅助判断;涉及重要决策时,请回到上方来源核实。