Go 1.27 的 encoding/json 底层换成 v2:默认拒绝非法 UTF-8 与重复 key,旧项目回退与兼容方案
项目信息
升级后先看这两处默认收紧
Go 1.27 把 encoding/json/v2 与 encoding/json/jsontext 收进标准库,不再需要 GOEXPERIMENT=jsonv2。v2 提供 Marshal / MarshalWrite / MarshalEncode 与 Unmarshal / UnmarshalRead / UnmarshalDecode 等入口,均接受可变 Options 参数。jsontext 是更底层的 JSON 语法包,Encoder / Decoder 以 Token / Value 为单位处理 JSON,并用内部状态机保证读入和产出的序列始终是合法 JSON。
对大多数老项目来说,这次升级真正的风险不在新 API,而在底层实现换成 v2 后,原本能静默处理的数据现在会直接报错:
- 非法 UTF-8:JSON 字符串里出现无效 UTF-8 字节(例如
0xff)时,v2 默认拒绝,不再像 v1 那样容忍。 - 重复 key:
{"name":"alice","name":"bob"}被视为非法 JSON,不再按 v1 的“后者覆盖前者”处理。
这些输入在历史数据、外部系统回调、代理转发的报文里都不少见。升级前最好先拿这类语料跑一遍回归。
v1 API 保留,但错误消息不再稳定
经典 encoding/json 包继续以 v1 API 提供,用户无需立刻迁移到新包。但它现在由 v2 实现支撑,行为上需要注意两点:
- 序列化 / 反序列化的整体行为目标是与 v1 对齐,但错误消息文本可能变化。
- v1 包新增了 Options,可配置为 v1 语义运行,因此遇到兼容性问题时,优先检查对应 Options 的开关状态。
如果直接用 v2 API,不传 Options 就是严格模式;要恢复接近 v1 的宽松行为,需要在调用点显式传入 Options。具体选项名称随 Go 1.27 版本可能继续调整,以官方文档为准。
最小复现:重复 key 与非法 UTF-8
package main
import (
"encoding/json"
"fmt"
)
func main() {
cases := []string{
`{"nickname":"alice","nickname":"bob"}`,
`{"nickname":"` + string([]byte{0xff}) + `"}`,
}
for _, raw := range cases {
var m map[string]string
err := json.Unmarshal([]byte(raw), &m)
fmt.Printf("input=%q\nmap=%#v\nerr=%v\n\n", raw, m, err)
}
}
用 Go 1.27 默认构建运行时,两个 case 都会在 json.Unmarshal 阶段返回错误:
go run main.go
# 第一个 case 报 duplicate key;
# 第二个 case 报 invalid UTF-8。
如果暂时需要保留 v1 行为,可以走退出开关:
GOEXPERIMENT=nojsonv2 go run main.go
此时重复 key 恢复“后值覆盖前值”,非法 UTF-8 也能按旧语义继续处理。
需要注意:GOEXPERIMENT 是构建期开关,不是运行时 flag。改动后必须重新执行 go build 并重新部署产物,CI 和本地缓存都应按两种模式区分开。
性能提升与临时退出开关
新实现序列化性能与 v1 大致相当,反序列化性能有明显提升。如果只是为了跟 Go 版本升级、暂不想动业务代码,不建议长期依赖 GOEXPERIMENT=nojsonv2——官方已说明这是一个退出选项,预计未来版本会移除。
务实的做法是:
- 新项目直接按 v2 严格模式写,提前暴露脏数据;
- 老项目先通过 v1/v2 的 Options 恢复 v1 语义,而不是全局退回旧实现;
- 回归测试里加入非法 UTF-8、重复 key 用例,确认错误处理和依赖错误文本的断言;
- 把
GOEXPERIMENT=nojsonv2当作紧急兜底,而不是项目长期配置。