五年了, JSON/V2 还是没有转正

2026 年 2 月 10 日,Go 1.26 重磅发布。Green Tea 垃圾回收器正式成为默认选项。go fix 工具经历了全面的现代化改造。cgo 调用开销降低了 30%。64 位平台迎来了堆地址随机化(Heap Address Randomization)安全增强。无论从哪个角度看,这都是一个里程碑式的版本。
然而,许多开发者翘首以盼的功能——encoding/json/v2——却明显缺席于稳定 API 之中。它依然躲在 GOEXPERIMENT=jsonv2 的实验性标志背后,尚未就绪。
我从Golang 1.25 就开始期待JSON/v2 了。五年的开发,居然还没完成?但如果你深入研究核心追踪议题 #76406 和相关提案 #71497,画面就变得清晰了:这种延迟不是工程效率的问题。它源于对 API 完美主义的执着追求、一场艰苦卓绝的向后兼容性攻坚战,以及一个几乎让整个项目脱轨的内存回退问题。
我们逐一拆解。

宏观视角:为什么 JSON V2 如此重要
当前的 encoding/json(我们姑且称之为 v1)已经忠实服务 Go 开发者超过十年。但它的设计缺陷一直在累积,就像一个从不重构的创业公司的技术债务。以下是主要问题清单:
- 它会默默接受无效的 UTF-8 JSON 字符串
- 它允许重复键名而毫无怨言
- 自定义
Marshaler实现无法访问配置选项 - 没有办法拒绝有效 JSON 文档之后的多余数据
- 所谓的"流式"API 基本上是个幌子——底层实际上会缓冲所有内容
这些绝非小问题。在生产环境中,它们是安全攻击向量和静默数据损坏的风险源。符合 RFC 8259 标准?差得远呢。
由 Joe Tsai 和 Daniel Martí 领导的 JSON v2,是自泛型(Generics)落地以来最具雄心的标准库重写。它旨在修复上述所有问题——同时带来巨大的性能提升。
全新架构:语法层与语义层的分离
v2 最精妙的设计决策之一,是将 JSON 处理严格拆分为两层:
encoding/json/jsontext—— 语法层(Syntactic Layer)。负责纯粹的 JSON 标记化(Tokenizing)、解析和编码。无反射,不涉及 Go 类型。你可以把它理解为一个高性能 JSON 扫描器。encoding/json/v2—— 语义层(Semantic Layer)。基于jsontext构建,实现 Go 类型与 JSON 数据之间的映射。
这种分离的威力在于:如果你只需要验证或转换原始 JSON 而不涉及 Go 结构体,可以直接使用 jsontext——零反射开销、真正的流式处理、最小化内存分配。
以下是两个版本的对比:
| 维度 | encoding/json (v1) | encoding/json/v2 (实验性) |
|---|---|---|
| 架构 | 语法与语义耦合,重度依赖反射 | 清晰分离:jsontext(语法) + v2(语义) |
| 流式处理 | 伪流式;底层往往需要全量缓冲 | 真正的流式编码与解码 |
| 默认安全性 | 接受无效 UTF-8,允许重复键名 | 拒绝无效 UTF-8,拒绝重复键名(符合 RFC 8259) |
| 性能 | 基准 | Unmarshal 最高提升 10 倍;Marshal 提升 1.6–3.6 倍 |
| unsafe 使用 | 无 | 无——在不使用 unsafe 的前提下达到第三方库级别的性能 |
最后一行值得着重强调。像 Sonic(字节跳动)这样的库通过大量使用 unsafe 来实现极致速度。而 JSON v2 在不牺牲内存安全的前提下达到了同等性能水平。这就是标准库的承诺:你不需要用正确性换取速度。

为什么没有随 1.26 发布:四大阻碍
既然 v2 这么好,为什么不在 Go 1.26 中发布?追踪议题 #76406 揭示了四个截然不同的战场。
1. “永久 API” 约束
在 Go 的标准库哲学中,一旦 API 脱离实验阶段进入 encoding/json 路径,它就必须受到 Go 1 兼容性承诺的保护。永远如此,不可撤回。
Joe Tsai 对此态度明确:性能问题可以后续通过优化来解决,但 API 设计缺陷一旦固化,就会成为未来几十年的技术债务。
目前的审计重点集中在 jsontext 的公共接口。作为基础层,它的设计需要在高性能与易用性之间取得平衡——尤其是在 Token 处理以及与 io.Reader/io.Writer 的最佳交互模式上。
此外还有关于 time.Duration 的激烈争论。v1 将持续时间序列化为纳秒整数——虽然广泛使用,但跨语言互操作性极差。v2 倾向于采用 Go 风格的字符串如 "1h2m3s",但这引发了关于标准化程度的热烈讨论。双方各执己见,API 签名悬而未决。
2. “完美兼容 json/v1
以下是 Go 团队的雄心壮志:一旦 v2 落地,他们希望只维护一套代码库。现有的 encoding/json 将变成 v2 引擎的一个薄包装层(Shim)。
问题在于:v2 引擎必须完美复刻 v1 的所有行为——包括 bug、未文档化的怪癖,以及成千上万生产应用无意中依赖的边缘情况。
例如,v1 在处理非寻址值(Non-addressable Values)的指针接收者 Marshaler 时存在特定的不一致性。许多现有应用在不知情的情况下已经依赖了这种行为。
在一个架构完全不同的引擎中复刻这些细微的、难以文档化的行为偏差,是一项极其艰巨的工程挑战。团队目前正在通过大规模测试逐步"烧除”(Burning Down)发现的每一个微小行为差异。进展稳步推进,但长尾效应依然明显。
3. 内存回退问题:Issue #75026
这是最令人警醒的阻碍。在 Go 1.25 和 1.26 的实验周期中,Issue #75026 报告了在特定 Map 序列化场景下灾难性的内存分配回退。
| 场景(Map 编码) | 内存分配 (B/op) — v1 原生 | 内存分配 (B/op) — v2 实验 | 增幅 |
|---|---|---|---|
| KeyCount: 10, ValLength: 1000 | 832 | 33,139 | +3,883% |
没有看错。一个常见的 Map 编码模式,内存分配量增加了 39 倍。
性能分析显示,92.98% 的内存分配集中在 bytes.growSlice 中,这表明新架构在处理复杂对象树或特定 Map 结构时存在严重的缓冲区管理缺陷。虽然总的分配次数(Allocs/op)实际上有所下降,但单次分配的体积暴增,给 GC 带来了巨大压力。
如果这种回退作为 v1 用户的默认行为发布,后果将是灾难性的。修复这些极端场景下的分配逻辑是目前最高优先级的工作。
4. 生态成熟度与联合类型之争
JSON v2 项目已经开发了五年。虽然已在许多生产环境中得到验证,但在成为默认标准之前,仍需经历更广泛的生态系统压力测试。Project 50 的追踪显示,44 个子任务中仍有约 18 个处于开放状态或需要进一步审计。
此外,关于 v2 是否应该支持 JSON 反序列化中的联合类型(Union Types / Sum Types),也引发了设计上的分歧。部分开发者认为这是现代 JSON 处理的标配功能。Go 团队的立场?等待语言层面的总和类型提案(如 #57644)成熟后再做集成,而不是在 JSON 包中塞入一个临时的、不兼容的实现。
这就是典型的 Go 式实用主义:不在库层面解决语言层面的问题。
值得等待的核心特性
尽管发布延迟,v2 的实验性版本已经展示了多项将从根本上改变 Go JSON 处理模式的特性。
omitzero —— 终于有了合理的省略逻辑
v1 的 omitempty 让开发者困惑了多年。time.Time{} 算空吗?false 算空吗?答案是不一致的,而且常常出人意料。
v2 引入了 omitzero,它严格按照 Go 的零值语义进行判断,并支持自定义 IsZero() bool 接口:
|
|
这对于 PATCH 风格的 API 尤其有用——你只需序列化那些被明确修改过的字段,而不会因为字段恰好持有类型的默认零值而意外忽略它们。
inline 和 unknown —— 灵活的数据建模
两个新的结构体标签解决了长期存在的痛点:
inline:将嵌套结构体或 Map 的内容"平铺"到父 JSON 对象中——无需匿名嵌入。对于包含动态键值对的 API 来说,这是一个巨大的改进。unknown:指定一个字段(通常是map[string]jsontext.Value)来捕获结构体中未定义的所有 JSON 成员。彻底告别"二次反序列化"的开销。
|
|
format —— 内置编码定制
format 选项支持按字段定制编码方式:
- 为
[]byte字段定制 Base64 或 Hex 编码 - 为
time.Time使用自定义布局字符串 - 不再需要为常见格式化需求编写自定义 Marshaler/Unmarshaler 实现
性能:Benchmark 数据说话
基于 jsonbench 评估套件,以下是 v2 与竞品的对比:
| 库 | Marshal 速度 | Unmarshal 速度 | 内存安全性 |
|---|---|---|---|
| JSON v1 | 基准 | 基准 | 高(无 unsafe) |
| JSON v2 | 提升 1.6–3.6 倍 | 提升 2.7–10.2 倍 | 高(无 unsafe) |
| Sonic(字节跳动) | 与 v2 相当 | 极快(部分使用 unsafe) | 中(大量使用 unsafe) |
| JSON-Iterator | 慢于 v2 | 慢于 v2 | 中 |
关键洞察:v2 通过迭代式线性解析实现性能飞跃,而非 v1 的逐字节虚函数扫描模式。而且它没有使用一个 unsafe.Pointer。这不仅仅是快——这是负责任地快。
前路:v2 何时落地?

基于当前的任务处理速度和 #76406 上的开发者活跃度,以下是务实的时间线: 2026 年上半年:
- 修复 Issue #75026 —— 解决 Map 编码的内存回退问题。这是脱离实验阶段的唯一最关键前提。
- API 定稿 —— 完成
jsontext中所有公共函数的审计,特别是Encoder/Decoder在流式场景中的状态机健壮性。 - v1 兼容层完善 —— 确保所有已知的 v1 行为(包括那些"有 bug 的"行为)在 v2 引擎中都有对应的配置选项支持。
Go 1.27(预计 2026 年 8 月):
这被广泛认为是 JSON v2 移除实验性标签、进入稳定标准库的最早也是最可能的窗口。1.27 开发周期中代码树(Tree)的重新开放将是需要密切关注的关键信号。
Go 团队还计划在 v2 正式发布时同步推出现代化迁移工具。重新设计的 go fix 将支持从 v1 到 v2 默认配置的一键迁移——不只是简单的字符串替换,而是感知类型信息的智能转换。例如,它能检测到手动实现的 Base64 转换逻辑,并建议替换为 v2 的 format:base64 结构体标签。
总结
- JSON v2 的延迟不是失败——而是纪律。 Go 团队拒绝发布一个存在设计缺陷的 API,因为在 Go 1 兼容性保证下,这些缺陷将永久存在。
- 架构设计是扎实的。 通过
jsontext和v2实现的语法/语义分离是一种精妙且面向未来的设计。 - 性能已经得到验证。 Unmarshal 最高提升 10 倍,Marshal 提升 3.6 倍——全程不使用
unsafe。 - 内存回退(#75026)是关键阻碍。 Map 编码中 39 倍的分配增长必须在 v2 成为默认之前解决。
- Go 1.27(2026 年 8 月)是目标。 社区应据此做好规划。
对开发者的实用建议:
- 内部工具:放心在非核心系统中尝试
GOEXPERIMENT=jsonv2。仅omitzero和unknown特性就能显著简化你的代码。提交 Bug 报告——团队需要它们。 - 性能敏感型应用:如果 JSON 反序列化是你的 CPU 瓶颈,v2 的实验版本可能已经优于 v1——而且比第三方
unsafe库更安全。 - 公共组件库:目前继续使用
encoding/json(v1)。v2 的 API 仍有可能发生破坏性变更,你的用户不会欣赏这种不稳定性。
JSON v2 将是 Go 语言自泛型以来最重要的库级演进。它不仅让速度更快——更补全了 Go 在现代数据交换标准合规性上长达十年的缺口。随着阻碍一个接一个地被清除,一个更安全、更快速、更灵活的 JSON 处理时代即将随 Go 1.27 到来。
等待即将结束。而且,值得。
参考资料:
- Go Issue #76406 — JSON v2 追踪议题
- Go Proposal #71497 — encoding/json/v2 提案
- Go Issue #75026 — jsonv2 实验中的内存分配回退
- Go Issue #57644 — 总和类型语言提案
- Go 1.26 Release Notes