内容提要
Go 1.27 将 encoding/json 底层替换为 v2,v1 变为薄胶水层,通过 DefaultOptionsV1 等选项实现逐字节兼容与渐进迁移。v2 修复了大小写敏感、重复键、非法 UTF-8 等 16 项历史缺陷,Unmarshal 到 any 提速约 2.25 倍,但启用 v1 兼容标志会拖慢性能。format 标签因等待类型化结构体标签被撤回,导致 time.Duration 无法原生序列化。
延伸解读
兼容性代价:v1 性能红利并非免费
文章实测显示,v1 底层虽已由 v2 驱动,但为复刻历史行为而启用的 AllowDuplicateNames 等标志位,会直接关闭 v2 针对 any 的优化快车道。单独开启该选项后,性能便从 v2 的约 2.25 倍提升跌回 v1 水平,内存分配次数也完全一致。这意味着继续调用 v1 API 的代码无法自动享受全部性能收益,若想榨干性能需显式迁移到 v2 原生接口。
迁移不是二选一:DefaultOptionsV1 提供渐进路径
v2 通过 DefaultOptionsV1 及单项覆盖选项,让开发者可以在 v1 与 v2 行为之间逐项切换。例如先传入 DefaultOptionsV1 兜底,再追加 AllowDuplicateNames(false) 即可在保留大部分 v1 行为的同时启用重复键拒绝。这种设计把过去非黑即白的兼容抉择变成了可调旋钮,但后传入选项优先级更高,迁移时需留意选项顺序与覆盖关系。
time.Duration 的尴尬:format 标签撤回后的死局
v2 决定不再默认序列化 time.Duration,要求显式指定格式,但承载该能力的 format 标签因等待类型化结构体标签而被撤回。结果是直接 Marshal 会报错,加上 format:units 同样报错,标准库内暂无原生手段序列化 time.Duration。开发者只能自行实现 Marshaler 或退回 v1,相关解析逻辑虽仍封存在内部包中,却无法从公共代码访问。
报错文案随机化:别让测试依赖错误字符串
v2 源码故意在 cannot 与 unable to 之间随机切换同一错误的措辞,每个进程固定一次。这是为防范海勒姆定律,避免下游代码把错误文本当作稳定接口。若单元测试使用 strings.Contains(err.Error(), "cannot marshal") 之类的断言,迁移到 v2 后可能随机失败。这并非缺陷,而是官方有意为之,测试应改为匹配错误类型或结构化信息。
Q&A
Go 1.27 中 encoding/json 和 encoding/json/v2 是什么关系?
Go 1.27 将 encoding/json(v1)的底层实现完全替换为 v2,v1 变成构建在 v2 之上的薄胶水层。v1 通过隐式注入 DefaultOptionsV1 等兼容选项来保持原有行为,因此现有代码无需修改即可继续运行,同时底层享受 v2 的引擎。
encoding/json/v2 修复了 v1 的哪些主要历史缺陷?
v2 修复了 16 项行为差异,主要包括:字段名匹配从大小写不敏感改为严格大小写敏感;重复键和非法 UTF-8 从静默容忍改为直接报错;nil slice/map 从输出 null 改为输出 [] 和 {};omitempty 判定从 Go 零值改为 JSON 空值;time.Duration 不再默认序列化为纳秒整数;指针接收者方法始终被调用;map 键也会触发自定义方法;不再保证 map 输出顺序等。
为什么 encoding/json/v2 要故意随机化报错文案?
这是为了防范海勒姆定律(Hyrum's Law)。v2 源码在 cannot 和 unable to 之间随机切换报错文案,目的是防止开发者对错误字符串进行正则或精确匹配,避免下游代码依赖报错文本这一可观测细节。该随机化利用 Go map 遍历顺序的随机性,并在每个进程内固定一次。
Go 1.27 中 time.Duration 为什么无法直接用 v2 序列化?
v2 决定不再保留 v1 将 time.Duration 序列化为纳秒整数的旧默认行为,要求调用方必须显式指定格式。但用于指定格式的 format 标签因等待未来的类型化结构体标签而被撤回,导致 v2 中直接序列化 time.Duration 会报错,开发者只能自行实现 Marshaler 或退回 v1。
使用 v1 兼容选项对性能有什么影响?
启用 v1 兼容标志会拖慢性能。例如反序列化到 any 时,v2 原生 API 比 v1 快约 2.25 倍,但若设置 AllowDuplicateNames(v1 默认容忍重复键),性能会跌回与 v1 相同水平,内存分配次数也从 17012 次增至 23014 次。因此想获得全部性能提升,必须显式调用 v2 API。
如何从 v1 渐进迁移到 v2?
可以混合使用 v1 和 v2 的选项进行渐进迁移。例如 jsonv2.Marshal(v, jsonv1.DefaultOptionsV1()) 语义上等价于 v1;在此基础上可追加单项覆盖,如 jsontext.AllowDuplicateNames(false) 启用 v2 的重复键拒绝,或 jsonv1.CallMethodsWithLegacySemantics(true) 保留 v1 的旧方法调用行为。后传入的选项优先级更高。