
老库不是坏了,是当年太宽松
Go 官方博客 11 月 4 日宣布,新写的两个包 encoding/json/v2 和 encoding/json/jsontext 在 Go 1.27 里转为稳定可用。原来那个 encoding/json 从 Go 1.0 之前就在标准库里,用了十五年,作者也承认它整体经得起考验。
问题出在取舍。它诞生时 JSON 还没那么严格,于是默认尽量接受、尽量不报错。RFC 8259 定稿后,这套宽松变成了隐患,官方博客列了四条:
接受非法 UTF-8,而标准要求必须是合法 UTF-8;接受同一个对象里出现重复的成员名;把 nil 切片和 nil 映射序列化成 null;反序列化时字段名按大小写不敏感匹配。
其中最值得说的是重复键。RFC 8259 没有规定重复成员名怎么处理,各实现可以任选一个、合并、丢弃或者报错——也就是说,同一段 JSON 在不同程序里可以有两种合法含义。官方点名说这可以被攻击者利用,并且已经发生过,例子是 CVE-2017-12635。
大小写不敏感匹配则被官方直接定性为 "令人意外的默认值、潜在的安全漏洞、性能限制"。这句话放在一个用了十五年的标准库上,分量不轻。
v2 到底改了什么
新版把职责切开:jsontext 只管语法,不碰反射;v2 负责 JSON 值与 Go 值之间的映射,并且是真流式,不再把整个值读进内存。
行为差异一共六条,都是把 "静默容忍" 换成 "明确报错":非法 UTF-8 报错;重复成员名报错;nil 切片和 nil 映射分别序列化成空数组和空对象;结构体字段名大小写敏感匹配;omitempty 改为 "编码后是 null、空串、空数组、空对象才省略";序列化 time.Duration 直接报错,因为原本就没有公认的表示方式,改由调用方用选项决定。
性能上,序列化基本持平,反序列化官方称最高快十倍。想吃满这个收益要改接口:把自定义的序列化方法换成流式版本。博客举了 Kubernetes 的例子,递归解析 OpenAPI 文档时旧接口把性能拖得很惨,换流式后提升到几个数量级。
迁移不是推倒重来
这次设计里最巧的一手,是 v1 在底层改用 v2 实现,并配上一整套兼容选项。于是 v1 的行为一点没变,却白得了新方法、新标签这些向后兼容的增强;两代包之间也互相认对方的序列化方法,依赖不会打架。
v2 的每条行为差异都对应一个开关,可以逐项挑选。官方给的例子是:想要 v2 的全部行为、但允许重复键,就在选项里显式打开;反过来想要 v1 行为、只禁重复键,也一样能配。这种颗粒度意味着迁移可以按模块做,不必等一次大重构。
对做企业数字化的小团队
"重复键" 听起来像学术问题,落到项目上很具体:两个系统对接同一份数据,一边取第一个值、一边取最后一个值,对账就会差一笔,而且查起来极难,因为两边都没报错。
新项目建议直接上 v2。它的实际价值不在快十倍,而在于把一类 "静默错" 变成 "启动就报错"——错误提前暴露,修复成本天差地别。
老项目不必动。v1 会一直支持,但至少要知道自己现在依赖的是哪种行为;在数据进出口这一层加校验,比在业务逻辑里到处兜底便宜得多。
还有一条是写文档时最容易漏的:给客户的接口说明里写清空值语义。null、空数组、空对象、字段不存在,这四种情况在对接里制造的扯皮,比任何性能问题都多。


评论交流
欢迎留下你的想法