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

张开发
2026/4/23 4:51:57 15 分钟阅读
Go 标准库 JSON 包迎来重大升级:encoding/json/v2 实验版来了
原文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。接受非法输入会导致静默的数据损坏。encoding/json目前接受含有重复成员名的 JSON 对象。这在安全场景下存在风险——历史上已有真实 CVECVE-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方法。Compact、Indent、HTMLEscape等函数只能写入bytes.Buffer不支持io.Writer。性能瓶颈MarshalJSON接口方法强制实现方分配并返回[]byte而encoding/json还需要再次验证和格式化这段 JSON。UnmarshalJSON需要先解析完整个 JSON 值才能确定边界然后调用方再解析一遍——相当于解析了两次。如果自定义的MarshalJSON/UnmarshalJSON方法内部递归调用Marshal/Unmarshal性能会退化为二次方级别。为什么不直接修改老包Go 团队不是没想过在原包里打补丁。问题是上述缺陷大多是 API 设计本身带来的而 Go 1 兼容性承诺明确规定现有代码的行为不能被破坏。在同一个包里新增MarshalV2、UnmarshalV2这类名字本质上只是在原包里建立一个平行命名空间治标不治本。所以答案只有一个建立独立的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 语法处理能力不依赖反射packagejsontexttypeEncoderstruct{...}funcNewEncoder(io.Writer,...Options)*Encoderfunc(*Encoder)WriteValue(Value)errorfunc(*Encoder)WriteToken(Token)errortypeDecoderstruct{...}funcNewDecoder(io.Reader,...Options)*Decoderfunc(*Decoder)ReadValue()(Value,error)func(*Decoder)ReadToken()(Token,error)Encoder和Decoder支持真正意义上的流式处理构造函数接受可变参数的 Options避免了 v1 中语法与语义混淆的问题。Token类型被重新设计可以表示任意 JSON token 而无需额外分配内存。v2 核心 APIpackagejsonfuncMarshal(in any,opts...Options)(out[]byte,errerror)funcMarshalWrite(out io.Writer,in any,opts...Options)errorfuncMarshalEncode(out*jsontext.Encoder,in any,opts...Options)errorfuncUnmarshal(in[]byte,out any,opts...Options)errorfuncUnmarshalRead(in io.Reader,out any,opts...Options)errorfuncUnmarshalDecode(in*jsontext.Decoder,out any,opts...Options)error函数签名与 v1 相似但每个函数都可以接受 Options 参数这是一个关键改进。不再需要先构造Encoder/Decoder再去读写io.Reader/io.Writer——MarshalWrite和UnmarshalRead直接支持。新的接口流式自定义序列化v2 保留了 v1 的Marshaler/Unmarshaler接口同时新增了更高效的流式版本typeMarshalerTointerface{MarshalJSONTo(*jsontext.Encoder)error}typeUnmarshalerFrominterface{UnmarshalJSONFrom(*jsontext.Decoder)error}这两个新接口允许实现方直接写入/读取Encoder/Decoder避免了中间的[]byte分配也解决了双重解析的性能问题。在 Kubernetes 的一个真实案例中OpenAPI 规范的递归解析使用UnmarshalJSON严重影响了性能切换到UnmarshalJSONFrom后性能提升了数个数量级。调用方自定义序列化这是 v2 的全新能力——调用方可以在不修改类型定义的情况下为任意类型指定自定义的 JSON 表示funcWithMarshalers(*Marshalers)OptionsfuncMarshalFunc[T any](fnfunc(TT any)([]byte,error))*MarshalersfuncMarshalToFunc[T any](fnfunc(*jsontext.Encoder,TT any)error)*MarshalersfuncWithUnmarshalers(*Unmarshalers)OptionsfuncUnmarshalFunc[T any](fnfunc([]byte,TT any)error)*UnmarshalersfuncUnmarshalFromFunc[T any](fnfunc(*jsontext.Decoder,TT any)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 选项inline、format以及流式接口会自动被 v1 继承无需改代码。降低维护成本一处修复两个版本同时受益无需单独 backport。v1 不会被废弃迁移是被鼓励的而非强制的。如何参与实验encoding/json/jsontext和encoding/json/v2目前是实验性包默认不可见。启用方式# 通过环境变量GOEXPERIMENTjsonv2 gotest./...在不修改任何代码的情况下在jsonv2实验模式下运行你的测试理论上不应有新的失败用例——因为 v1 的底层实现已被替换为 v2但对外行为在 Go 1 兼容性范围内保持一致。如果发现问题可以在 go.dev/issue/71497 上反馈。这个实验的结果将决定 v2 的命运——从被放弃到作为稳定包进入 Go 1.26都有可能。小结encoding/json/v2是 Go 社区历时 5 年、经过大量实际生产验证的成果由许多非 Google 员工主导开发体现了 Go 作为开放社区项目的本质。核心改进点更严格的 JSON 语法校验nil 值序列化更符合直觉大小写敏感匹配更安全Options 参数统一透传解决了长期 API 割裂问题流式接口消除性能瓶颈Unmarshal 性能最高提升 10 倍。如果你的项目重度依赖 JSON 序列化现在是参与测试、提供反馈的好时机。参考资料go.dev/blog/jsonv2-exp

更多文章