---
id: 61f2009278a5f7366f79395da59447cf
author: lemonmint
title: Go 1.25 encoding/json/v2 行为变更及 v1 兼容层分析
description: 比较 Go 1.25 实验性功能(goexperiment.jsonv2)中引入的 encoding/json/v2 在大小写敏感匹配、omitempty/omitzero 语义变化、nil 切片/映射序列化及选项方面的差异。
language: zh
date: 2025-06-20T05:38:20.100520237Z
path: /blog/posts/go-1-25-encoding-json-v1-vs-v2-z74a36b08
category: release
---

Go 的 `encoding/json` 包 v2 是一种全新的实现,旨在改进 v1 的若干缺点(一致性不足、出人意料的行为、性能问题)。它通过 `goexperiment.jsonv2` 构建标签启用,属于实验性功能。

最重要的一点是,**启用 v2 后,v1 将作为一个兼容层运行,在 v2 实现之上模拟 v1 的行为**。这是通过 `v2_v1.go` 文件中的 `DefaultOptionsV1()` 函数实现的。也就是说,v2 提供了能够完整复现 v1 行为的选项,同时也提出了更严格、更可预测的新默认行为。

v2 的主要目标如下:

1. **提高正确性和可预测性**:默认采用更严格的规则(例如区分大小写、禁止重复键),以减少意外行为。
2. **提升性能**:重新设计解析和编码引擎,以提高效率。
3. **扩展灵活性和控制权**:引入详细的选项系统,使开发者能够精细地控制 JSON 处理方式。

---

## 主要语义/行为差异

以 `v2_test.go` 文件为中心,按项目整理了 v1 与 v2 的行为差异。

### 1. 字段名称匹配(区分大小写)

- **v1 行为**:将 JSON 对象成员反序列化到 Go 结构体字段时,**忽略大小写(case-insensitive)**进行匹配。`"FirstName"` 和 `"firstname"` 都会映射到 `FirstName` 字段。
- **v2 行为**:默认**区分大小写(case-sensitive)**,仅映射与名称完全一致的字段。
- **变更原因**:忽略大小写的匹配可能导致意外行为,并且在处理不匹配的字段时会带来性能损失。v2 采用了更清晰、更可预测的默认行为。
- **相关选项**:在 v2 中,可以使用 `json:"...,case:ignore"` 标签选项为单个字段显式启用忽略大小写,也可以全局应用 `json.MatchCaseInsensitiveNames(true)` 选项。

### 2. `omitempty` 标签选项含义的变化

- **v1 行为**:根据 Go 值的"空状态"省略字段。这里的"空状态"指 `false`、`0`、`nil` 指针/接口,以及长度为 0 的数组/切片/映射/字符串。
- **v2 行为**:根据编码后的 JSON 值的"空状态"省略字段。即编码结果为 `null`、`""`、`{}` 或 `[]` 时省略。
- **变更原因**:v1 的定义依赖于 Go 的类型系统。v2 以 JSON 类型系统为准,从而提供更一致的行为。例如,在 v1 中 `bool` 类型的 `false` 值会被省略,而在 v2 中 `false` 是非空的 JSON 值,因此不会被省略。v2 新增了 `omitzero` 选项,可以替代 v1 中 `omitempty` 对 `0` 或 `false` 的处理方式。
- **相关选项**:若希望在 v2 中保持与 v1 相同的行为,可使用 `json.OmitEmptyWithLegacyDefinition(true)` 选项。

### 3. `string` 标签选项行为的变化

- **v1 行为**:适用于数字、布尔和字符串类型字段。将对应值重新编码为 JSON 字符串中的内容(例如 `int(42)` -> `"42"`)。不会递归应用于复合类型(切片、映射等)内部的值。
- **v2 行为**:**仅适用于数字类型**,并且会递归应用。也就是说,`[]int` 等切片内部的数字也都会被编码为 JSON 字符串。
- **变更原因**:`string` 选项的主要用途是将数字表示为字符串,以防止 64 位整数的精度丢失。v1 的行为有限且缺乏一致性。v2 专注于这一核心用途,并将其递归扩展,使其更加实用。
- **相关选项**:可以使用 `json.StringifyWithLegacySemantics(true)` 选项模拟 v1 的行为。

### 4. `nil` 切片和映射的序列化

- **v1 行为**:`nil` 切片和 `nil` 映射被序列化为 `null`。
- **v2 行为**:默认情况下,`nil` 切片被序列化为 `[]`(空数组),`nil` 映射被序列化为 `{}`(空对象)。
- **变更原因**:`nil` 是 Go 语言的实现细节,将其暴露在与语言无关的 JSON 格式中并不可取。表示空集合的 `[]` 和 `{}` 是更通用的表示方式。
- **相关选项**:在 v2 中,可以通过 `json.FormatNilSliceAsNull(true)` 或 `json.FormatNilMapAsNull(true)` 选项使其像 v1 一样序列化为 `null`。

### 5. 数组反序列化

- **v1 行为**:反序列化到 Go 数组(`[N]T`)时,即使 JSON 数组的长度与 Go 数组的长度不同也不会报错。如果长度较短,多余的位置用零值填充;如果较长,超出部分会被丢弃。
- **v2 行为**:JSON 数组的长度必须与 Go 数组的长度**完全一致**,否则会报错。
- **变更原因**:在 Go 中,固定大小数组的长度通常具有重要意义。v1 的行为可能导致数据被无声丢失。v2 通过更严格的规则提高了正确性。
- **相关选项**:可以使用 `json.UnmarshalArrayFromAnyLength(true)` 选项模拟 v1 的行为。

### 6. `time.Duration` 处理

- **v1 行为**:`time.Duration` 在内部被视为 `int64`,并编码为以纳秒为单位的 JSON 数字。
- **v2 行为**:使用 `time.Duration.String()` 方法编码为 `"1h2m3s"` 等格式的 JSON 字符串。
- **变更原因**:纳秒数字的可读性较差,而 `time.Duration` 的标准字符串表示更加实用。
- **相关选项**:可以通过 `json:",format:nano"` 标签选项或 `json.FormatTimeWithLegacySemantics(true)` 选项使用 v1 的行为。

### 7. 无效 UTF-8 的处理

- **v1 行为**:在序列化/反序列化时,如果字符串中包含无效的 UTF-8 字节,会**静默替换**为 Unicode 替换字符(`\uFFFD`)。
- **v2 行为**:默认情况下,遇到无效 UTF-8 时**返回错误**。
- **变更原因**:防止数据被无声损坏,并遵循更严格的 JSON 标准(RFC 7493)。
- **相关选项**:可以使用 `jsontext.AllowInvalidUTF8(true)` 选项模拟 v1 的行为。

### 8. 重复的对象成员名称处理

- **v1 行为**:允许 JSON 对象中出现同名的重复成员。后出现的值会覆盖先前的值。
- **v2 行为**:默认情况下,如果存在重复的成员名称,则**返回错误**。
- **变更原因**:RFC 8259 标准未定义重复名称的行为,因此不同实现可能表现不同。这可能成为安全漏洞的根源。v2 明确拒绝此类输入,以提高正确性和安全性。
- **相关选项**:可以使用 `jsontext.AllowDuplicateNames(true)` 选项模拟 v1 的行为。

---

## 实现与架构差异

- **v1**:主要依赖 `decode.go` 中的 `decodeState` 以及 `scanner.go` 中手写的状态机(state machine)。这是一种解析逻辑与语义分析紧密耦合的单体结构。
- **v2**:架构更加模块化。
  - **`encoding/json/jsontext`**:提供底层、高性能的 JSON 词法分析器(`Decoder`)和编码器(`Encoder`)。该包仅关注 JSON 的语法层面。
  - **`encoding/json/v2`**:基于 `jsontext`,负责处理 Go 类型与 JSON 值之间的语义转换。
  - 通过这种分离,语法分析与语义分析彼此独立,代码的清晰度和性能均得到提升。

---

## v2 的新 API 与功能

v2 通过 `json.Options` 系统提供了非常灵活的控制功能。

- **`json.Options`**:一组用于改变序列化/反序列化行为的选项的集合。
- **`json.JoinOptions(...)`**:将多个选项合并为一个。
- **`WithMarshalers` / `WithUnmarshalers`**:无需实现 `Marshaler`/`Unmarshaler` 接口,即可为特定类型注入序列化/反序列化逻辑的强大功能。这在处理外部包中的类型时尤其有用。
- **新增选项**:`RejectUnknownMembers`、`Deterministic(false)`、`FormatNilSliceAsNull` 等,使得许多 v1 无法实现的行为控制成为可能。

---

## 结论

`encoding/json` v2 基于 v1 的经验,在**正确性、性能和灵活性**方面都有显著提升,是一种现代化的实现。虽然默认行为变得更加严格,但通过完善的 `Options` 系统,可以完整支持 v1 的所有行为,因此可以在保持与现有代码兼容的同时,逐步引入 v2 的优势。

- 对于**新项目**,建议默认使用 v2。
- 对于**现有项目**,可以继续直接使用 `jsonv1`,或者在迁移到 `jsonv2` 的过程中,通过 `DefaultOptionsV1()` 逐步引入 v2 的严格行为。
