编程 encoding/json/v2 实验版:Go 标准库 JSON 包的重大升级来了

2026-09-04 15:59:03

cover

原文:A new experimental Go API for JSON

作者:Joe Tsai、Daniel Martí 等 Go 核心团队成员


背景:一个用了 15 年的老包

JSON 是当前互联网上最主流的数据交换格式,encoding/json 是 Go 标准库中第 5 个被引用最多的包。

这个包已经稳定服务了将近 15 年。对任意 Go 类型进行序列化和反序列化的设计思路,加上可自定义的表示方式,整体表现不错。

但 15 年下来,随着 JSON 规范完善和社区需求演进,encoding/json 的一些缺陷逐渐难以忽视。受制于 Go 1 兼容性承诺,这些问题无法在现有包里修复。

于是有了 encoding/json/v2

老版本有哪些问题?

行为缺陷

  1. 对 JSON 语法的处理不够严格

    • encoding/json 目前接受非法的 UTF-8 字符。RFC 8259(最新的 JSON 互联网标准)要求有效的 UTF-8,接受非法输入会导致静默的数据损坏。
    • 接受含有重复成员名的 JSON 对象。这在安全场景下有风险,历史上已有真实 CVE(CVE-2017-12635)利用过这一点。
  2. nil slice 和 map 序列化为 null

    社区调查显示,大多数 Go 开发者希望 nil slice 和 nil map 默认序列化为空数组 [] 和空对象 {},而不是 null。当前行为与其他语言的 JSON 实现交互时容易产生兼容问题。

  3. 大小写不敏感的反序列化

    当前版本在将 JSON 字段名映射到 Go struct 字段时默认大小写不敏感。这既令人意外,也是潜在的安全隐患,还影响性能。

  4. 方法调用的不一致性

    指针接收者上的 MarshalJSON 被调用的行为存在不一致。这是公认的 bug,但由于太多应用依赖当前行为,已无法修复。

API 设计的局限性

  • json.NewDecoder(r).Decode(v) 这种惯用写法无法检测输入末尾的多余内容。
  • 选项只能设置在 Encoder/Decoder 上,无法传入 Marshal/Unmarshal 函数,也无法向下透传给自定义的 MarshalJSON/UnmarshalJSON 方法。
  • CompactIndentHTMLEscape 等函数只能写入 bytes.Buffer,不支持 io.Writer

性能瓶颈

  • MarshalJSON 接口强制实现方分配并返回 []byteencoding/json 还需要再次验证和格式化这段 JSON。
  • UnmarshalJSON 需要先解析完整个 JSON 值才能确定边界,调用方再解析一遍——解析了两次。
  • 自定义 MarshalJSON/UnmarshalJSON 内部递归调用 Marshal/Unmarshal 时,性能会退化为二次方级别。

为什么不直接修改老包?

Go 团队不是没想过在原包里打补丁。问题是,上述缺陷大多来自 API 设计本身,而 Go 1 兼容性承诺要求现有代码行为不能被破坏。

在同一个包里新增 MarshalV2UnmarshalV2 这类名字,本质上只是在原包里建立平行命名空间,治标不治本。

所以选择独立 v2 命名空间,即 encoding/json/v2

架构设计:语法与语义分离

v2 的核心设计决策是将 JSON 处理拆成两层:

  • 语法层(Syntactic):只关心 JSON 的格式和语法,不涉及 Go 类型含义。用 encode/decode 描述。
  • 语义层(Semantic):定义 JSON 值与 Go 值之间的映射关系。用 marshal/unmarshal 描述。

语法层由新的 encoding/json/jsontext 包实现,语义层由 encoding/json/v2 实现,后者构建在前者之上。

encoding/json/jsontext

这个包提供纯粹的 JSON 语法处理能力,不依赖反射:

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)

EncoderDecoder 支持真正的流式处理,构造函数接受可变参数的 Options,避免 v1 中语法与语义混淆的问题。Token 类型被重新设计,可以表示任意 JSON token,无需额外分配内存。

v2 核心 API

package json
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error

函数签名与 v1 相似,但每个函数都能接受 Options 参数,这是关键改进。不再需要先构造 Encoder/Decoder 再读写 io.Reader/io.Writer——MarshalWriteUnmarshalRead 直接支持。

新接口:流式自定义序列化

v2 保留 v1 的 Marshaler/Unmarshaler 接口,同时新增更高效的流式版本:

type MarshalerTo interface {
    MarshalJSONTo(*jsontext.Encoder) error
}
type UnmarshalerFrom interface {
    UnmarshalJSONFrom(*jsontext.Decoder) error
}

这两个接口允许实现方直接写入/读取 Encoder/Decoder,避免中间的 []byte 分配,也解决双重解析的性能问题。

在 Kubernetes 的真实案例中,OpenAPI 规范的递归解析用 UnmarshalJSON 严重影响性能,切换到 UnmarshalJSONFrom 后性能提升了数个数量级。

调用方自定义序列化

这是 v2 的新能力——调用方可以在不修改类型定义的情况下,为任意类型指定自定义 JSON 表示:

func WithMarshalers(*Marshalers) Options
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers

例如,可以让所有 proto.Message 类型的序列化统一交由 protojson 处理,只需在调用 Marshal 时传入一个 Option,无需修改 proto 类型本身。

v2 的行为变化

v2 设计目标是在直接迁移时大部分行为保持一致,但以下几点有明确变化:

行为v1v2
无效 UTF-8静默接受报错
重复 JSON 键静默接受报错
nil slice/map 序列化null[] / {}
struct 字段匹配大小写不敏感大小写敏感
omitempty 语义基于 Go 零值基于 JSON 空值(null、""、[]、{})
time.Duration 序列化输出整数报错(需显式指定格式)

对大多数行为变化,可以通过 struct tag 或 Options 参数回退到 v1 语义,迁移路径是渐进式的。

性能表现

  • Marshal:与 v1 大体持平,略有快慢之分。
  • Unmarshal:明显快于 v1,基准测试显示最高可达 10 倍提升。

想获得更大性能收益,建议把现有 Marshaler/Unmarshaler 实现同时实现 MarshalerTo/UnmarshalerFrom,充分利用流式处理的优势。

v1 与 v2 的关系

Go 团队不希望标准库中长期存在两套 JSON 实现,因此计划让 v1 在底层由 v2 实现。这带来三个好处:

  • 渐进迁移:通过 Options 灵活混搭 v1 和 v2 的行为语义,而不是非此即彼。
  • 功能继承:v2 新增特性(如新的 struct tag 选项 inlineformat,以及流式接口)会自动被 v1 继承,无需改代码。
  • 降低维护成本:一处修复,两个版本同时受益,无需单独 backport。

v1 不会被废弃,迁移是被鼓励的,不是强制的。

如何参与实验

encoding/json/jsontextencoding/json/v2 目前是实验性包,默认不可见。启用方式:

# 通过环境变量
GOEXPERIMENT=jsonv2 go test ./...

在不修改代码的情况下,在 jsonv2 实验模式下运行你的测试,理论上不应出现新的失败用例——因为 v1 底层实现已被替换为 v2,对外行为在 Go 1 兼容性范围内保持一致。

如果发现问题,可以在 go.dev/issue/71497 反馈。实验结果将决定 v2 的命运——从被放弃到作为稳定包进入 Go 1.26,都有可能。

小结

encoding/json/v2 是 Go 社区历时 5 年、经过大量生产验证的成果,由许多非 Google 员工主导开发。核心改进:更严格的 JSON 语法校验,nil 值序列化更符合直觉,大小写敏感匹配更安全,Options 参数统一透传解决了 API 割裂问题,流式接口消除性能瓶颈,Unmarshal 性能最高提升 10 倍。

如果项目重度依赖 JSON 序列化,现在是参与测试、提供反馈的时机。

复制全文 生成海报 Go encoding json JSON

推荐文章

程序员茄子在线接单