JSON 是互联网世界中最普遍的数据交换格式之一,也是 Go 标准库里使用最广泛的组件之一。
但很多人可能不知道,Go 团队为了重写 encoding/json(也就是本文多次提到的 JSON v2),前后花了将近六年时间。
从 2020 年的最初构想,到 2023 年的社区讨论,再到 2025 年 Go 1.25 的实验性发布,最终在 2026 年 Go 1.27 中正式稳定,encoding/json/v2 总算走完了这段漫长旅程。
这不是一次简单的性能优化,而是一次涉及 API 设计、历史兼容性、底层架构、社区协作和生态迁移的系统性重构。
Go 团队究竟遇到了什么问题?为什么不能直接修复旧版?六年时间又沉淀出了怎样的工程经验?
2026 年 11 月 4 日,Go 官方博客发布了一篇新文章:A new Go API for JSON。文章并没有带来什么全新的 API,也不是一次普通的 Go 1.27 特性介绍。它更像是一份迟到了六年的项目总结:Go 团队终于可以正式宣布,encoding/json/v2 和 encoding/json/jsontext 已经在 Go 1.27 中稳定发布。
有意思的是,JSON 本身并不复杂,它只有对象、数组、字符串、数字、布尔值和 null 几种基本结构。那么,为什么一个 JSON 库的重写,竟然需要如此漫长的时间?
答案藏在 Go 标准库的历史包袱、API 设计以及 Go 社区的工程文化之中。
一个不能轻易修改的老牌标准库
Go 对 JSON 的支持,远比很多人想象得更早。在 Go 1.0 正式发布之前,标准库就已经提供了 encoding/json。十多年过去,它早已成为 Go 生态最重要的基础组件之一。
Go 官方在 2025 年的实验版介绍中提到,encoding/json 已经是 Go 项目中被导入次数排名第五的包。然而,使用范围越广,重构的代价也就越大。
经过多年发展,旧版 encoding/json 暴露出了几个明显的设计缺陷:
- 标准合规性不足:接受非法 UTF-8 数据,也允许 JSON 对象出现重复字段名。
- 默认行为不够合理:nil slice 和 nil map 会被编码成
null,而不是空数组和空对象。
- 字段匹配过于宽松:反序列化时采用大小写不敏感匹配。
- API 扩展受限:自定义
MarshalJSON 和 UnmarshalJSON 方法难以获得调用方配置的选项。
- 流式处理能力不足:传统接口需要先构造完整的 JSON 字节切片,难以真正做到高效流式处理。
这些问题并不是最近才被发现的,Go 社区早已围绕它们讨论多年。为什么不直接修复?这里涉及 Go 最重要的工程原则之一:Go 1 兼容性承诺。
一个被大量项目依赖的标准库,其既有行为已经成为事实上的 API 契约。比如,旧版允许大小写不敏感匹配,如果突然改成严格匹配,可能导致某些一直正常运行的业务系统无法解析原有数据。同样,nil slice 从 null 改成 [],虽然在很多场景下更合理,却依然会改变程序输出。
这些改变可能是正确的,但不能简单地塞进一个已有十多年历史的 API 中。Go 团队最终意识到:有些问题不是修复几个 Bug 就能解决的,而是需要重新设计 API。于是,JSON v2 诞生了。
从 2020 到 2026:一场持续六年的重构
这项工作的历程,实际上比 Go 1.25 实验版发布要早得多。从 Go 官方博客披露的信息中,可以还原出一条相对完整的时间线。

这条时间线里有两个值得特别关注的人物。Daniel Martí 是 encoding/json 的维护者之一,他最早从旧版 API 的设计局限出发,思考如何构建一个全新版本。Joe Tsai 则有另一条技术线索——在参与 Go Protobuf API 的工作后,他发现 protojson 不得不使用独立的 JSON 实现,因为旧版 encoding/json 无法同时满足更严格的 JSON 标准要求和高效流式处理需求。
两人最终走到一起,开始构建新的 JSON 实现。随后,Roger Peppe、Chris Hines、Johan Brandhorst-Satzkorn、Damien Neil、Austin Clements、Michael Pratt 等人陆续参与设计评审、代码审查和回归测试。Go 官方特别强调,这项工作很大程度上由 Google 之外的开发者推动和贡献,这也是 Go 开源项目协作模式的一个缩影。
真正的重构:把 JSON 处理拆成两个层次
如果说前两部分讲的是为什么要重写,以及重写经历了什么,那么这一部分才是 JSON v2 最重要的技术成果。
过去,很多开发者会把 JSON 序列化理解成一个简单的过程:Go 对象转换成 JSON 字符串,或者反过来。但从实现角度来看,这里面其实包含两个不同的问题:
- JSON 本身是否合法?如何解析 JSON 语法?
- JSON 数据应该如何映射到 Go 类型?
旧版 encoding/json 将这两部分逻辑交织在一起,导致 API 扩展、性能优化和行为调整都受到限制。JSON v2 最重要的架构调整,就是把它们彻底分离。
第一层:encoding/json/jsontext 负责 JSON 语法层面的处理。它不关心 Go 的 struct、反射或者类型映射,只关注 JSON 本身。比如读取和写入 JSON Token,解析和生成完整 JSON Value,处理编码与解码,以及提供真正的流式处理能力。
第二层:encoding/json/v2 负责 JSON 与 Go 类型之间的语义映射。比如 Go struct 如何映射到 JSON Object,Go slice 如何映射到 JSON Array,如何处理自定义 Marshaler,以及如何通过 Options 控制序列化行为。

这种分层带来的价值远不止代码结构更加清晰。首先,语法层不再依赖 Go 的反射机制,可以作为一个独立的 JSON 语法 处理组件使用;其次,语义层不再需要自己承担所有底层 JSON 解析工作,能够更灵活地实现类型映射。更重要的是,双方可以独立演进,这也是为什么 JSON v2 不仅仅是一次代码重写,而是一次底层架构的重新设计。
六年重构,还有一个更难的任务:兼容过去
对于一个新项目来说,重新设计 API 并不困难。真正困难的是:如何让新 API 和旧 API 在同一个生态中长期共存?
Go 团队在 JSON v2 设计中做了一个非常关键的决定:旧版 encoding/json 并不会被废弃,而是使用新版实现作为底层引擎。换句话说,encoding/json 和 encoding/json/v2 并不是两套完全独立的 JSON 实现,而是「底层统一,行为分流」。

这样设计有几个好处。第一,避免维护两套实现——未来修复底层 Bug、优化性能或者增加新能力,不需要分别维护两套 JSON 引擎。第二,允许逐步迁移——开发者可以通过 Options 选择不同的行为组合,不必一次性接受所有行为变化。第三,让旧版也能受益——例如新版提供了 MarshalJSONTo 和 UnmarshalJSONFrom 接口,即使某个项目仍然通过 v1 调用 JSON 序列化,也可以在底层使用这些新接口。
这意味着 Go 团队没有要求整个生态立即迁移到 v2,而是尽可能降低迁移成本。官方也明确表示,旧版 encoding/json 将继续得到支持,不会因为 v2 的发布而被废弃。这其实是一个非常值得借鉴的工程决策:重构基础设施,不应该把迁移成本简单地转嫁给使用者。
小结:六年之后,Go 团队交付的不只是一个 JSON 库
Go 1.27 中的 JSON v2,终于从实验性功能变成了正式标准库 API。从结果来看,它解决了旧版长期存在的设计问题,也带来了更灵活的 API 和更好的性能表现。根据官方公布的基准测试,v2 的 Marshal 性能总体与 v1 接近,而 Unmarshal 在部分场景下最高可以达到 10 倍性能提升。不过,Go 团队也没有回避性能上的不足:部分场景仍然慢于 v1,后续 Go 1.28 还会继续优化。
对于开发者而言,现在并不需要急着把所有项目迁移到 v2。Go 官方已经提供了专门的迁移指南,旧版 API 也会继续得到支持。真正值得关注的,是这项工作背后的工程方法。
回顾这六年,Go 团队并没有因为旧 API 存在缺陷就急于推倒重来。他们先识别问题,再重新设计 API;先构建原型,再邀请社区参与;先通过实验让真实项目检验,再最终承诺稳定性;同时还要考虑如何让新旧实现长期共存。这是一种相当克制的工程演进方式。
尤其是在今天,AI 让代码生成和重构的速度越来越快,写出一个新的 JSON 库或许并不困难。但如何保证它能被数百万个 Go 项目安全使用十年甚至更久,这才是真正困难的事情。六年时间,Go 团队重写的不只是 JSON 库,更是为一个成熟生态重新设计基础设施的方式。在云栈社区的技术讨论中,这类大型基础设施重构案例也一直是开发者热议的话题。
参考资料:
- A new Go API for JSON
- A new experimental Go API for JSON
- encoding/json/v2 Migration Guide
- Go JSON v2 — Hacker News 讨论
- Go 1.27 Release Notes