Go JSON 的演变:从 v1 到 v2

本文翻译自 Go JSON v2,版权归原作者所有。

Go 1.25 中即将推出的 json 包的第二个版本是一次重大更新,包含许多破坏性变更。v2 包添加了新特性,修复了 API 问题和行为缺陷,并提升了性能。让我们来看看都有哪些变化!

使用 MarshalUnmarshal 的基本用例保持不变。以下代码在 v1 和 v2 中都能正常工作:

type Person struct {
    Name string
    Age  int
}

alice := Person{Name: "Alice", Age: 25}

// 序列化 Alice
b, err := json.Marshal(alice)
fmt.Println(string(b), err)

// 反序列化 Alice
err = json.Unmarshal(b, &alice)
fmt.Println(alice, err)

但其余部分有很大不同。让我们来看看 v1 的主要变化。

MarshalWrite 和 UnmarshalRead

在 v1 中,你使用 Encoder 序列化到 io.Writer,使用 Decoderio.Reader 反序列化:

// 序列化 Alice
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder) // io.Writer
enc := json.NewEncoder(out)
enc.Encode(alice)
fmt.Println(out.String())

// 反序列化 Bob
in := strings.NewReader(`{"Name":"Bob","Age":30}`) // io.Reader
dec := json.NewDecoder(in)
var bob Person
dec.Decode(&bob)
fmt.Println(bob)

从现在开始,我将省略错误处理以保持简洁。

在 v2 中,你可以直接使用 MarshalWriteUnmarshalRead,无需任何中间媒介:

// 序列化 Alice
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder)
json.MarshalWrite(out, alice)
fmt.Println(out.String())

// 反序列化 Bob
in := strings.NewReader(`{"Name":"Bob","Age":30}`)
var bob Person
json.UnmarshalRead(in, &bob)
fmt.Println(bob)

但它们并不完全可互换:

  • MarshalWrite 不添加换行符,而旧版 Encoder.Encode 会添加。
  • UnmarshalRead 从 reader 读取所有内容直到遇到 io.EOF,而旧版 Decoder.Decode 只读取下一个 JSON 值。

MarshalEncode 和 UnmarshalDecode

EncoderDecoder 类型已经移至新的 jsontext 包,且它们的接口已经发生了重大变化(以支持低级流式编码/解码操作)。

你可以将它们与 json 函数一起使用,以读写 JSON 流,类似于之前 EncodeDecode 的工作方式:

  • v1 Encoder.Encode → v2 json.MarshalEncode + jsontext.Encoder
  • v1 Decoder.Decode → v2 json.UnmarshalDecode + jsontext.Decoder

流式编码器:

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())

流式解码器:

in := strings.NewReader(`
{"Name":"Alice","Age":25}
{"Name":"Bob","Age":30}
{"Name":"Cindy","Age":15}
`)
dec := jsontext.NewDecoder(in)

for {
    var p Person
    // 每次调用解码一个 Person 对象
    err := json.UnmarshalDecode(dec, &p)
    if err == io.EOF {
        break
    }
    fmt.Println(p)
}

UnmarshalRead 不同,UnmarshalDecode 以完全流式方式工作,每次调用解码一个值,而不是读取所有内容直到 io.EOF

选项

选项用于配置序列化和反序列化函数的特定功能:

  • FormatNilMapAsNullFormatNilSliceAsNull 定义如何编码 nil 映射和切片。
  • MatchCaseInsensitiveNames 允许匹配 Namename 等。
  • Multiline 将 JSON 对象展开为多行。
  • OmitZeroStructFields 从输出中省略零值字段。
  • SpaceAfterColonSpaceAfterComma 在每个 :, 后添加空格。
  • StringifyNumbers 将数字类型表示为字符串。
  • WithIndentWithIndentPrefix 缩进嵌套属性(注意 MarshalIndent 函数已被移除)。

每个序列化或反序列化函数可以接受任意数量的选项:

alice := Person{Name: "Alice", Age: 25}
b, _ := json.Marshal(
    alice,
    json.OmitZeroStructFields(true),
    json.StringifyNumbers(true),
    jsontext.WithIndent(" "),
)
fmt.Println(string(b))

你也可以使用 JoinOptions 组合选项:

alice := Person{Name: "Alice", Age: 25}
opts := json.JoinOptions(
    jsontext.SpaceAfterColon(true),
    jsontext.SpaceAfterComma(true),
)
b, _ := json.Marshal(alice, opts)
fmt.Println(string(b))

查看文档中的完整选项列表:有些在 json 包中,有些在 jsontext 包中。

标签

v2 支持在 v1 中定义的字段标签:

  • omitzeroomitempty 用于省略空值。
  • string 将数字类型表示为字符串。
  • - 用于忽略字段。

并添加了一些新的标签:

  • case:ignorecase:strict 指定如何处理大小写差异。
  • format:template 根据模板格式化字段值。
  • inline 通过将嵌套对象的字段提升到父级来扁平化输出。
  • unknown 为未知字段提供"全部捕获"。

下面是演示 inlineformat 的示例:

type Person struct {
    Name      string    `json:"name"`
    // 格式化日期为 yyyy-mm-dd
    BirthDate time.Time `json:"birth_date,format:DateOnly"`
    // 将 Address 字段内联到 Person 对象中
    Address   `json:",inline"`
}

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}

func main() {
    alice := Person{
        Name:      "Alice",
        BirthDate: time.Date(2001, 7, 15, 12, 35, 43, 0, time.UTC),
        Address: Address{
            Street: "123 Main St",
            City:   "Wonderland",
        },
    }
    b, _ := json.Marshal(alice, jsontext.WithIndent(" "))
    fmt.Println(string(b))
}

还有 unknown

type Person struct {
    Name string `json:"name"`
    // 将所有未知的 Person 字段收集到 Data 字段中
    Data map[string]any `json:",unknown"`
}

func main() {
    src := `{
    "name": "Alice",
    "hobby": "adventure",
    "friends": [
        {"name": "Bob"},
        {"name": "Cindy"}
    ]
}`
    var alice Person
    json.Unmarshal([]byte(src), &alice)
    fmt.Println(alice)
}

自定义序列化

使用 MarshalerUnmarshaler 接口的基本自定义序列化用例保持不变。此代码在 v1 和 v2 中都能正常工作:

// 一个自定义布尔类型,表示为 "✓" 表示 true,"✗" 表示 false
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
}

func main() {
    // 序列化
    val := Success(true)
    data, err := json.Marshal(val)
    fmt.Println(string(data), err)

    // 反序列化
    src := []byte(`"✓"`)
    err = json.Unmarshal(src, &val)
    fmt.Println(val, err)
}

然而,Go 标准库文档推荐使用新的 MarshalerToUnmarshalerFrom 接口(它们以纯流式方式工作,这可能快得多):

// 一个自定义布尔类型,表示为 "✓" 表示 true,"✗" 表示 false
type Success bool

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
}

func main() {
    // 序列化
    val := Success(true)
    data, err := json.Marshal(val)
    fmt.Println(string(data), err)

    // 反序列化
    src := []byte(`"✓"`)
    err = json.Unmarshal(src, &val)
    fmt.Println(val, err)
}

更好的是,你不再局限于特定类型的单一序列化方式。现在,你可以在需要时使用自定义序列化器和反序列化器,使用通用的 MarshalFuncUnmarshalFunc 函数。

func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers

例如,你可以将 bool 值序列化为 而无需创建自定义类型:

// 用于布尔值的自定义序列化器
boolMarshaler := json.MarshalFunc(
    func(val bool) ([]byte, error) {
        if val {
            return []byte(`"✓"`), nil
        }
        return []byte(`"✗"`), nil
    },
)

// 通过 WithMarshalers 选项传递自定义序列化器给 Marshal
val := true
data, err := json.Marshal(val, json.WithMarshalers(boolMarshaler))
fmt.Println(string(data), err)

并将 反序列化为 bool

// 用于布尔值的自定义反序列化器
boolUnmarshaler := json.UnmarshalFunc(
    func(data []byte, val *bool) error {
        *val = string(data) == `"✓"`
        return nil
    },
)

// 通过 WithUnmarshalers 选项传递自定义反序列化器给 Unmarshal
src := []byte(`"✓"`)
var val bool
err := json.Unmarshal(src, &val, json.WithUnmarshalers(boolUnmarshaler))
fmt.Println(val, err)

也有 MarshalToFuncUnmarshalFromFunc 函数用于创建自定义序列化器。它们类似于 MarshalFuncUnmarshalFunc,但使用 jsontext.Encoderjsontext.Decoder 而不是字节切片。

func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers

你可以使用 JoinMarshalers 组合序列化器(使用 JoinUnmarshalers 组合反序列化器)。例如,这里展示了如何将布尔值(true/false)和"类布尔"字符串(on/off)序列化为 ,同时保留所有其他值的默认序列化方式。

首先定义一个布尔值的自定义序列化器:

// 将布尔值序列化为 ✓ 或 ✗
boolMarshaler := json.MarshalToFunc(
    func(enc *jsontext.Encoder, val bool) error {
        if val {
            return enc.WriteToken(jsontext.String("✓"))
        }
        return enc.WriteToken(jsontext.String("✗"))
    },
)

然后定义一个类布尔字符串的自定义序列化器:

// 将类布尔字符串序列化为 ✓ 或 ✗
strMarshaler := json.MarshalToFunc(
    func(enc *jsontext.Encoder, val string) error {
        if val == "on" || val == "true" {
            return enc.WriteToken(jsontext.String("✓"))
        }
        if val == "off" || val == "false" {
            return enc.WriteToken(jsontext.String("✗"))
        }

        // SkipFunc 是一种特殊类型的错误,告诉 Go 跳过当前序列化器
        // 并继续下一个。在我们的例子中,下一个将是字符串的默认序列化器。
        return json.SkipFunc
    },
)

最后,使用 JoinMarshalers 组合序列化器,并通过 WithMarshalers 选项将它们传递给序列化函数:

// 使用 JoinMarshalers 组合自定义序列化器
marshalers := json.JoinMarshalers(boolMarshaler, strMarshaler)

// 序列化一些值
vals := []any{true, "off", "hello"}
data, err := json.Marshal(vals, json.WithMarshalers(marshalers))
fmt.Println(string(data), err)

这不是很酷吗?

默认行为

v2 不仅改变了包接口,还改变了默认的序列化/反序列化行为。

一些值得注意的序列化差异包括:

  • v1 将 nil 切片序列化为 null,v2 序列化为 []。你可以使用 FormatNilSliceAsNull 选项更改它。
  • v1 将 nil 映射序列化为 null,v2 序列化为 {}。你可以使用 FormatNilMapAsNull 选项更改它。
  • v1 将字节数组序列化为数字数组,v2 序列化为 base64 编码的字符串。你可以使用 format:arrayformat:base64 标签更改它。
  • v1 允许字符串内的无效 UTF-8 字符,v2 不允许。你可以使用 AllowInvalidUTF8 选项更改它。

以下是 v2 默认行为的示例:

type Person struct {
    Name    string
    Hobbies []string
    Skills  map[string]int
    Secret  [5]byte
}

func main() {
    alice := Person{
        Name:   "Alice",
        Secret: [5]byte{1, 2, 3, 4, 5},
    }
    b, _ := json.Marshal(alice, jsontext.Multiline(true))
    fmt.Println(string(b))
}

以下是如何强制 v1 行为:

type Person struct {
    Name    string
    Hobbies []string
    Skills  map[string]int
    Secret  [5]byte `json:",format:array"`
}

func main() {
    alice := Person{
        Name:   "Alice",
        Secret: [5]byte{1, 2, 3, 4, 5},
    }
    b, _ := json.Marshal(
        alice,
        json.FormatNilMapAsNull(true),
        json.FormatNilSliceAsNull(true),
        jsontext.Multiline(true),
    )
    fmt.Println(string(b))
}

一些值得注意的反序列化差异包括:

  • v1 使用不区分大小写的字段名匹配,v2 使用精确的区分大小写匹配。你可以使用 MatchCaseInsensitiveNames 选项或 case 标签更改它。
  • v1 允许对象中的重复字段,v2 不允许。你可以使用 AllowDuplicateNames 选项更改它。

以下是 v2 默认行为(区分大小写)的示例:

type Person struct {
    FirstName string
    LastName  string
}

func main() {
    src := []byte(`{"firstname":"Alice","lastname":"Zakas"}`)
    var alice Person
    json.Unmarshal(src, &alice)
    fmt.Printf("%+v\n", alice)
}

以下是如何强制 v1 行为(不区分大小写):

type Person struct {
    FirstName string
    LastName  string
}

func main() {
    src := []byte(`{"firstname":"Alice","lastname":"Zakas"}`)
    var alice Person
    json.Unmarshal(
        src, &alice,
        json.MatchCaseInsensitiveNames(true),
    )
    fmt.Printf("%+v\n", alice)
}

文档中可以查看行为变化的完整列表。

性能

在序列化时,v2 与 v1 的表现大致相同。它对某些数据集更快,但对其他数据集更慢。然而,反序列化要好得多:v2 比 v1 快 2.7 倍到 10.2 倍。

此外,通过从常规的 MarshalJSONUnmarshalJSON 切换到它们的流式替代方案 — MarshalJSONToUnmarshalJSONFrom,可以获得显著的性能提升。根据 Go 团队的说法,它可以将某些 O(n²) 运行时场景转换为 O(n)。例如,在 k8s OpenAPI 规范中从 UnmarshalJSON 切换到 UnmarshalJSONFrom 使其速度提高了约 40 倍

有关基准测试详情,请参阅 jsonbench 仓库。

最终思考

呼!这里有太多要吸收的内容。v2 包比 v1 有更多功能且更灵活,但它也更复杂,特别是考虑到分成 json/v2jsontext 子包。

需要记住的几点:

  • 截至 Go 1.25,json/v2 包仍处于实验阶段,可以通过在构建时设置 GOEXPERIMENT=jsonv2 来启用。包 API 可能在未来的版本中发生变化。
  • 开启 GOEXPERIMENT=jsonv2 会让 v1 json 包使用新的 JSON 实现,这更快并支持一些选项,以便与旧的序列化和反序列化行为更好地兼容。

最后,以下是一些了解 v2 设计和实现的链接:

提案第一部分提案第二部分json/v2jsontext

comments powered by Disqus