Go 标准库 JSON 包迎来重大升级:encoding/json/v2 实验版来了
原文:A new experimental Go API for JSON 作者:Joe Tsai、Daniel Martí 等 Go 核心团队成员 JSON 是当今互联网上最主流的数据交换格式,而 这个包已经稳定服务了将近 15 年。总体来说,它表现不错——对任意 Go 类型进行序列化和反序列化的设计思路,加上可自定义的表示方式,被证明具有很强的灵活性。 但 15 年并不短。随着 JSON 规范的不断完善、社区需求的持续演进, 于是, 1. 对 JSON 语法的处理不够严格 2. nil slice 和 map 序列化为 社区调查显示,大多数 Go 开发者希望 nil slice 和 nil map 默认序列化为空数组 3. 大小写不敏感的反序列化 当前版本在将 JSON 字段名映射到 Go struct 字段时,默认是大小写不敏感的。这既令人意外,又是一个潜在的安全隐患,同时还影响性能。 4. 方法调用的不一致性 指针接收者上的 Go 团队不是没想过在原包里打补丁。问题是,上述缺陷大多是 API 设计本身带来的,而 Go 1 兼容性承诺明确规定,现有代码的行为不能被破坏。 在同一个包里新增 所以,答案只有一个:建立独立的 v2 的一个核心设计决策是将 JSON 处理拆分为两层: 语法层由新的 这个包提供了纯粹的 JSON 语法处理能力,不依赖反射: 函数签名与 v1 相似,但每个函数都可以接受 Options 参数,这是一个关键改进。不再需要先构造 v2 保留了 v1 的 这两个新接口允许实现方直接写入/读取 在 Kubernetes 的一个真实案例中,OpenAPI 规范的递归解析使用 这是 v2 的全新能力——调用方可以在不修改类型定义的情况下,为任意类型指定自定义的 JSON 表示: 例如,可以让所有 v2 的设计目标是在直接迁移时大部分行为保持一致,但以下几点有明确变化: 对于大多数行为变化,都可以通过 struct tag 或 Options 参数回退到 v1 语义,迁移路径是渐进式的。 想要获得更大的性能收益,建议将现有的 Go 团队不希望标准库中同时存在两套 JSON 实现,因此计划让 v1 在底层由 v2 实现。这带来三个好处: 渐进迁移:可以通过 Options 灵活混搭 v1 和 v2 的行为语义,而不是非此即彼。 功能继承:v2 新增的特性(如新的 struct tag 选项 降低维护成本:一处修复,两个版本同时受益,无需单独 backport。 v1 不会被废弃,迁移是被鼓励的,而非强制的。 在不修改任何代码的情况下,在 如果发现问题,可以在 go.dev/issue/71497 上反馈。这个实验的结果将决定 v2 的命运——从被放弃到作为稳定包进入 Go 1.26,都有可能。 如果你的项目重度依赖 JSON 序列化,现在是参与测试、提供反馈的好时机。背景:一个用了 15 年的老包
encoding/json 是 Go 标准库中第 5 个被引用最多的包。encoding/json 的一些缺陷逐渐变得难以忽视,而且受制于 Go 1 兼容性承诺,这些问题根本无法在现有包里修复。encoding/json/v2 应运而生。老版本有哪些问题?
行为缺陷
encoding/json 目前接受非法的 UTF-8 字符。RFC 8259(最新的 JSON 互联网标准)明确要求有效的 UTF-8。接受非法输入会导致静默的数据损坏。encoding/json 目前接受含有重复成员名的 JSON 对象。这在安全场景下存在风险——历史上已有真实 CVE(CVE-2017-12635)利用过这一点。null[] 和空对象 {},而不是 null。当前行为在与其他语言的 JSON 实现交互时,容易引起兼容问题。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,性能会退化为二次方级别。为什么不直接修改老包?
MarshalV2、UnmarshalV2 这类名字,本质上只是在原包里建立一个平行命名空间,治标不治本。v2 命名空间,也就是 encoding/json/v2。架构设计:语法与语义分离
encoding/json/jsontext 包实现,语义层由 encoding/json/v2 实现,后者构建在前者之上。encoding/json/jsontext
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)Encoder 和 Decoder 支持真正意义上的流式处理,构造函数接受可变参数的 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) errorEncoder/Decoder 再去读写 io.Reader/io.Writer——MarshalWrite 和 UnmarshalRead 直接支持。新的接口:流式自定义序列化
Marshaler/Unmarshaler 接口,同时新增了更高效的流式版本:type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}Encoder/Decoder,避免了中间的 []byte 分配,也解决了双重解析的性能问题。UnmarshalJSON 严重影响了性能,切换到 UnmarshalJSONFrom 后性能提升了数个数量级。调用方自定义序列化
func WithMarshalers(*Marshalers) Options
func MarshalFunc[T any](fn func(T "T any") ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T "T any") error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
func UnmarshalFunc[T any](fn func([]byte, T "T any") error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T "T any") error) *Unmarshalersproto.Message 类型的序列化统一交由 protojson 包处理,只需在调用 Marshal 时传入一个 Option 即可,无需修改 proto 类型本身。v2 的行为变化
行为 v1 v2 无效 UTF-8 静默接受 报错 重复 JSON 键 静默接受 报错 nil slice/map 序列化 null[] / {}struct 字段匹配 大小写不敏感 大小写敏感 omitempty 语义基于 Go 零值 基于 JSON 空值(null、""、[]、{}) time.Duration 序列化输出整数 报错(需显式指定格式) 性能表现
Marshaler/Unmarshaler 实现同时也实现 MarshalerTo/UnmarshalerFrom,以充分利用流式处理的优势。v1 与 v2 的关系
inline、format,以及流式接口)会自动被 v1 继承,无需改代码。如何参与实验
encoding/json/jsontext 和 encoding/json/v2 目前是实验性包,默认不可见。启用方式:# 通过环境变量
GOEXPERIMENT=jsonv2 go test ./...jsonv2 实验模式下运行你的测试,理论上不应有新的失败用例——因为 v1 的底层实现已被替换为 v2,但对外行为在 Go 1 兼容性范围内保持一致。小结
encoding/json/v2 是 Go 社区历时 5 年、经过大量实际生产验证的成果,由许多非 Google 员工主导开发,体现了 Go 作为开放社区项目的本质。核心改进点:更严格的 JSON 语法校验,nil 值序列化更符合直觉,大小写敏感匹配更安全,Options 参数统一透传解决了长期 API 割裂问题,流式接口消除性能瓶颈,Unmarshal 性能最高提升 10 倍。