Go 1.25 即将带来 json 包的第二个版本。这个版本并非小修小补:它新增了能力,修正了 API 与行为层面的缺陷,也改善了性能。不过,它同时包含不少不兼容变更。
本文按原文梳理 encoding/json/v2 与 v1 的关键差异,重点关注迁移时最容易踩到的接口、默认行为和性能变化。文中的示例可配合原文的交互式环境练习。
最常见的 Marshal 与 Unmarshal 用法在两个版本中保持一致:把结构体编码为 JSON 字节,再把 JSON 解码回结构体。
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 25}
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
err = json.Unmarshal(b, &alice)
fmt.Println(alice, err)
这段代码在 v1 与 v2 中都可以工作。真正需要注意的是,它之外的大部分边缘行为和扩展接口都已经变化。
io.Writer 与 io.Reader
在 v1 中,向 io.Writer 编码或从 io.Reader 解码通常要经过 Encoder 与 Decoder:
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder)
enc := json.NewEncoder(out)
enc.Encode(alice)
fmt.Println(out.String())
in := strings.NewReader(`{"Name":"Bob","Age":30}`)
dec := json.NewDecoder(in)
var bob Person
dec.Decode(&bob)
fmt.Println(bob)
在 v2 中,可以直接使用 MarshalWrite 和 UnmarshalRead,中间对象不再是必需的:
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder)
json.MarshalWrite(out, alice)
fmt.Println(out.String())
in := strings.NewReader(`{"Name":"Bob","Age":30}`)
var bob Person
json.UnmarshalRead(in, &bob)
fmt.Println(bob)
这两组接口不能机械替换。MarshalWrite 不会像旧的 Encoder.Encode 一样自动追加换行。相应地,UnmarshalRead 会一直读取到 io.EOF,而旧的 Decoder.Decode 每次只读取下一个 JSON 值。如果调用方依赖逐值处理流式输入,这个差异会直接影响行为。
jsontext 处理流v2 将 Encoder 和 Decoder 移到新的 jsontext 包,并调整了接口,以支持更底层的流式编解码操作。原来的映射关系如下:
Encoder.Encode 对应 v2 的 json.MarshalEncode 加 jsontext.Encoder
Decoder.Decode 对应 v2 的 json.UnmarshalDecode 加 jsontext.Decoder
下面是持续写入多个 JSON 值的示例:
people := []Person{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
{Name: "Cindy", Age: 15},
}
out := new(strings.Builder)
enc := jsontext.NewEncoder(out)
for _, p := range people {
json.MarshalEncode(enc, p)
}
fmt.Print(out.String())
逐个读取 JSON 值时,UnmarshalDecode 才是对应接口:
in := strings.NewReader(`
{"Name":"Alice","Age":25}
{"Name":"Bob","Age":30}
{"Name":"Cindy","Age":15}
`)
dec := jsontext.NewDecoder(in)
for {
var p Person
err := json.UnmarshalDecode(dec, &p)
if err == io.EOF {
break
}
fmt.Println(p)
}
UnmarshalDecode 是真正的逐值流式解码:每次调用只解出一个值,不会为了读完输入而等待 io.EOF。 当数据来自长连接、连续记录流或大型输入时,应明确区分它与 UnmarshalRead。
v2 将许多可选行为统一为函数参数。常用选项包括:
FormatNilMapAsNull 与 FormatNilSliceAsNull:控制 nil map、nil slice 的编码结果MatchCaseInsensitiveNames:允许 Name 与 name 这类字段名忽略大小写匹配Multiline:把 JSON 对象展开成多行OmitZeroStructFields:省略零值结构体字段SpaceAfterColon 与 SpaceAfterComma:在 :、, 后加入空格StringifyNumbers:把数值类型编码为字符串WithIndent、WithIndentPrefix:控制嵌套属性的缩进原文特别指出,MarshalIndent 已被移除,缩进通过选项完成:
alice := Person{Name: "Alice", Age: 25}
b, _ := json.Marshal(
alice,
json.OmitZeroStructFields(true),
json.StringifyNumbers(true),
jsontext.WithIndent(" "),
)
fmt.Println(string(b))
若需要组合多个设置,可以使用 JoinOptions:
opts := json.JoinOptions(
jsontext.SpaceAfterColon(true),
jsontext.SpaceAfterComma(true),
)
b, _ := json.Marshal(alice, opts)
fmt.Println(string(b))
选项并不都在同一个包中。json 与 jsontext 都提供了各自的选项集合,迁移时应按功能查阅对应包的文档。
v2 继续支持 v1 的字段标签:omitzero、omitempty、string 与 -。同时新增了几类标签:
case:ignore 或 case:strict:指定字段名大小写差异的处理方式format:template:按模板格式化字段值inline:把嵌套对象的字段提升到父对象unknown:收集未知字段inline 与 format 可以一起使用。例如,日期以 DateOnly 输出,同时把地址字段扁平化到 Person:
type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,format:DateOnly"`
Address `json:",inline"`
}
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
unknown 则适合演进中的协议。它让结构体接住未声明的字段,而不是把这些数据悄悄丢掉:
type Person struct {
Name string `json:"name"`
Data map[string]any `json:",unknown"`
}
当输入含有 hobby、friends 等未知字段时,这些内容会进入 Data。unknown 为前向兼容的 JSON 模型提供了一个明确的兜底位置。
基于 Marshaler 和 Unmarshaler 的基本自定义方式仍然可用。比如,可以把自定义布尔类型编码为 ✓ 或 ✗:
type Success bool
func (s Success) MarshalJSON() ([]byte, error) {
if s {
return []byte(`"✓"`), nil
}
return []byte(`"✗"`), nil
}
func (s *Success) UnmarshalJSON(data []byte) error {
*s = string(data) == `"✓"`
return nil
}
不过,标准库文档建议优先使用流式的 MarshalerTo 和 UnmarshalerFrom。对应方法直接面向 jsontext.Encoder 与 jsontext.Decoder:
func (s Success) MarshalJSONTo(enc *jsontext.Encoder) error {
if s {
return enc.WriteToken(jsontext.String("✓"))
}
return enc.WriteToken(jsontext.String("✗"))
}
func (s *Success) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
tok, err := dec.ReadToken()
*s = tok.String() == `✓`
return err
}
如果只想在某个调用点改变类型的编码方式,不必为此定义新类型。 v2 通过泛型 MarshalFunc 和 UnmarshalFunc 提供可组合的自定义处理器:
boolMarshaler := json.MarshalFunc(
func(val bool) ([]byte, error) {
if val {
return []byte(`"✓"`), nil
}
return []byte(`"✗"`), nil
},
)
data, err := json.Marshal(true, json.WithMarshalers(boolMarshaler))
fmt.Println(string(data), err)
与之对应,UnmarshalFunc 可以在解码时把 ✓ 或 ✗ 映射回 bool。对于流式场景,还可以使用 MarshalToFunc 与 UnmarshalFromFunc。
多个处理器可以借助 JoinMarshalers 或 JoinUnmarshalers 组合。一个处理器若返回 json.SkipFunc,框架会跳过它并继续尝试下一个处理器,最后仍可回退到默认处理逻辑。
v2 的变化不只发生在 API 上,默认编码和解码行为也与 v1 不同。下面这些差异最值得在迁移测试中显式覆盖。
编码方面:
null,v2 编码为 [];需要旧行为时使用 FormatNilSliceAsNull
null,v2 编码为 {};需要旧行为时使用 FormatNilMapAsNull
format:array 与 format:base64 标签调整AllowInvalidUTF8
type Person struct {
Name string
Hobbies []string
Skills map[string]int
Secret [5]byte
}
alice := Person{
Name: "Alice",
Secret: [5]byte{1, 2, 3, 4, 5},
}
b, _ := json.Marshal(alice, jsontext.Multiline(true))
fmt.Println(string(b))
上例在 v2 中会把未初始化的 slice、map 输出为 []、{},字节数组则会输出为 Base64 字符串。若业务协议仍要求 v1 格式,可以组合 FormatNilMapAsNull、FormatNilSliceAsNull,并给数组字段加 json:",format:array"。
解码方面,v1 默认忽略字段名大小写,而 v2 默认精确、区分大小写地匹配字段名。例如把 firstname 解码进 FirstName,v2 默认不会匹配;需要旧行为时可传入 json.MatchCaseInsensitiveNames(true),或通过 case 标签逐字段声明。v1 也允许同一个对象中出现重复字段,v2 默认不允许;需要兼容时可使用 AllowDuplicateNames。
编码性能方面,v2 与 v1 大体相当:有些数据集更快,有些更慢。解码提升更明显,原文给出的范围是 v2 比 v1 快约 2.7~10.2 倍。
把常规的 MarshalJSON、UnmarshalJSON 改为流式 MarshalJSONTo、UnmarshalJSONFrom 还可能带来额外收益。原文引用 Go 团队的说明:在特定场景中,这类改造可把 O(n²) 的运行时问题降为 O(n);Kubernetes OpenAPI 规范曾出现约 40 倍的提升案例。
性能数据不能替代迁移验证。对已有服务,至少应覆盖:协议中 nil 容器的形态、字节数组表示、字段名匹配规则、重复字段输入,以及一次读取一个值还是读取完整输入的语义。v2 的价值不只是更快,而是让编码规则和兼容策略从隐式默认值变成可显式声明的选择。
截至原文所述的 Go 1.25,json/v2 仍是实验性能力,需要在构建时设置 GOEXPERIMENT=jsonv2。其 API 仍可能在后续版本变化。
启用该实验还会让 v1 的 json 包采用新的 JSON 实现。这样既能获得更快的实现,也能使用部分选项来更好地兼容旧的编码与解码行为。
迁移时可以先从边界最清晰的调用开始:明确区分整段读取与流式读取,为外部协议补足兼容选项,再以真实载荷做回归测试。这样才能既利用 v2 的新能力,也避免默认值变化在上线后变成难以定位的协议问题。
收录于 FunTester 原创专题:我的语言迁徙:Java & Groovy & Go
相关阅读:JSON 基础 · JSON 必知必会【PDF+ 视频教程】 · 复杂 JSON 结构创建语法 · 使用 jq 处理 JSON 数据(一) · 有了 Groovy,我们还需要 JsonPath 吗?