Go JSON 序列化完全指南

Go 阅读约 13 分钟
GoJSON序列化encoding/json性能优化

深入掌握 Go 语言 JSON 序列化技术,涵盖 struct tag 配置、自定义序列化方法、流式编解码、性能优化以及常见陷阱与解决方案。

Go 与 JSON 的类型映射

encoding/json 是 Go 标准库提供的 JSON 处理包。它基于反射实现,无需外部依赖即可完成绝大多数 JSON 任务。理解 Go 类型和 JSON 类型之间的映射规则是正确使用该包的前提。

Go 类型JSON 类型行为说明
booltrue/false直接映射
stringstring自动处理 Unicode 转义
int/float 系列numberint64 超大值用 string tag 保精度
[]Tarraynil 切片输出 null
map[string]Tobject键必须为 string 类型
*T(指针)null/值nil 指针输出 null
structobject仅导出字段参与序列化

有几个重要规则值得特别注意:只有首字母大写的导出字段才会被序列化,未导出字段会被静默忽略;nil 指针和 nil 切片序列化为 null,但零值结构体字段(空字符串、0、false)默认都会被输出,除非使用 omitempty 标签抑制。

json.Marshal 与 json.Unmarshal 基础

json.Marshal 接收任意 Go 值并返回 JSON 字节切片,json.Unmarshal 执行反向转换。这是 Go 中最基础的 JSON 操作。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"`
}

func main() {
    u := User{Name: "张三", Age: 28}

    // 序列化:返回 []byte 和 error
    data, err := json.Marshal(u)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))
    // {"name":"张三","age":28}

    // 反序列化:第二个参数必须是指针类型
    var u2 User
    err = json.Unmarshal(data, &u2)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", u2) // {Name:张三 Age:28 Email:}
}

json.Unmarshal 的第二个参数必须传入指针。如果传入值类型,反序列化的数据会被写入一个不可见的副本,调用者完全无法感知,这是 Go 新手最高频的错误之一。编译器不会对此报错,因为 interface{} 接受任何类型。好在大部分 IDE 和 linter 插件的静态分析可以捕捉到这种错误。

Struct Tag 完全解析

struct tag 是 Go JSON 序列化的核心配置机制,也是 Go 语言 “标签驱动开发” 哲学的典型体现。所有行为在 struct 定义时一次性声明完毕,无需额外的 XML 或 JSON 配置文件。

type Product struct {
    // 基础重命名:JSON 键名 "product_id"
    ID int64 `json:"product_id"`

    // omitempty:零值时省略该字段
    // 零值定义:数值 = 0, 字符串 = "", 指针/map/切片 = nil, bool = false
    Description string `json:"description,omitempty"`

    // string:强制序列化为 JSON 字符串(避免大整数精度丢失)
    SKU int64 `json:"sku,string"`

    // "-"(连字符):完全忽略,不序列化也不反序列化
    InternalCode string `json:"-"`

    // "-,"(连字符加逗号):字段名就是 "-"
    Meta string `json:"-,"`

    // 嵌入结构体(匿名字段):字段提升到父级
    AuditInfo
}

type AuditInfo struct {
    CreatedAt string `json:"created_at"`
    UpdatedAt string `json:"updated_at"`
}

omitempty 的判断基于 Go 的零值语义,而不是自定义的空值定义。例如,一个被赋值为 ""string 字段在 omitempty 下不会输出,但一个赋值为 0int 字段同样不会输出——即使 0 本身是业务上的合法值。对于这种场景,改用指针类型 *int 可以让 nil0 明确区分:nil 被省略,0 正常输出。

自定义序列化:MarshalJSON 与 UnmarshalJSON

实现 json.Marshalerjson.Unmarshaler 接口可以完全接管某个类型的 JSON 表示。这是 Go 中处理自定义类型最灵活的方式。

import (
    "encoding/json"
    "time"
)

type Duration struct {
    time.Duration
}

// 序列化为人类可读格式:"5s"、"2m30s" 等
func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Duration.String())
}

// 从字符串还原 Duration
func (d *Duration) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    dur, err := time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration %q: %w", s, err)
    }
    d.Duration = dur
    return nil
}

// 使用示例
type Config struct {
    Timeout Duration `json:"timeout"`
}

cfg := Config{Timeout: Duration{5 * time.Second}}
data, _ := json.Marshal(cfg)
fmt.Println(string(data)) // {"timeout":"5s"}

注意 UnmarshalJSON 方法必须定义在指针接收者上(*Duration),否则反序列化的数据无法被写入原对象。自定义序列化的常见场景包括:时间格式处理(如 yyyy-MM-dd 而非 ISO 8601)、枚举常量转换、数值单位修饰、敏感字段脱敏、以及带版本的序列化逻辑。

流式处理:json.Encoder 与 json.Decoder

处理 HTTP 请求体或大型日志文件时,应使用流式 API 避免在内存中中转整个 JSON,从而降低内存峰值并提升吞吐。

import (
    "encoding/json"
    "net/http"
    "os"
)

// 从 HTTP 请求体中安全地反序列化并做校验
func HandleCreateUser(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()

    decoder := json.NewDecoder(r.Body)
    // 拒绝 JSON 中存在但目标结构体不认识的字段
    decoder.DisallowUnknownFields()

    var user User
    if err := decoder.Decode(&user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // 业务处理...
}

// 将大量数据流式写入文件
func WriteUsersToFile(users []User, filename string) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()

    enc := json.NewEncoder(f)
    enc.SetIndent("", "  ")
    return enc.Encode(users)
}

json.Decoder 还支持 UseNumber() 方法,将 JSON 中的数字解析为 json.Number(本质是字符串)而非 float64,从而避免大整数精度丢失和浮点误差。DisallowUnknownFields() 对 API 严格校验非常有用,能尽早暴露客户端发送了多余字段的问题。

json.RawMessage 延迟解析

当 JSON 中某个字段的类型取决于同一结构中另一个字段的值时(多态消息),json.RawMessage 允许暂缓解析,先提取类型判别字段,再根据类型决定后续的解析目标。

type Message struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"` // 原始 JSON 字节,暂不解析
}

type TextPayload struct {
    Content string `json:"content"`
}

type ImagePayload struct {
    URL    string `json:"url"`
    Width  int    `json:"width"`
    Height int    `json:"height"`
}

func ParseMessage(data []byte) (any, error) {
    var msg Message
    if err := json.Unmarshal(data, &msg); err != nil {
        return nil, err
    }

    switch msg.Type {
    case "text":
        var p TextPayload
        if err := json.Unmarshal(msg.Payload, &p); err != nil {
            return nil, err
        }
        return p, nil
    case "image":
        var p ImagePayload
        if err := json.Unmarshal(msg.Payload, &p); err != nil {
            return nil, err
        }
        return p, nil
    default:
        return nil, fmt.Errorf("unknown message type: %q", msg.Type)
    }
}

这种模式在 WebSocket 消息分发、消息队列消费、配置文件的多态 section 解析中非常常见。它避免了两次全量解析的性能开销:外层只解析到 Payload 为止(拿到原始字节),内层按需选择性解析。

常见陷阱与应对

Go 的 JSON 处理有一类颇具 Go 特色的陷阱,理解其根本原因便不难规避。

// 陷阱1: nil slice vs empty slice —— 序列化结果不同
type Collection struct {
    Items []string `json:"items"`
}

c1 := Collection{}                  // Items 为 nil
c2 := Collection{Items: []string{}} // Items 为空切片(长度为 0 的非 nil 切片)

j1, _ := json.Marshal(c1) // {"items":null}
j2, _ := json.Marshal(c2) // {"items":[]}

// 最佳实践:统一使用 make([]T, 0) 初始化切片
// 或使用 omitempty 标签统一行为(都省略)

// 陷阱2: time.Time 格式固定为 RFC 3339
type Event struct {
    Time time.Time `json:"time"`
}
// 输出:2026-07-15T10:30:00Z(无法通过 tag 改变格式)
// 自定义格式必须实现 MarshalJSON/MarshalText 接口或使用自定义类型

// 陷阱3: map 的键顺序不可靠
m := map[string]int{"c": 3, "a": 1, "b": 2}
data, _ := json.Marshal(m)
// 每次运行的输出顺序可能不同!
// 根本原因:Go 的 map 遍历顺序是故意随机化的

map 键顺序问题源于 Go 运行时的 map 迭代顺序本身就是随机的(这是安全策略,防止代码依赖 map 迭代顺序)。标准化解决方案是按 keys 排序后逐个写入,或直接使用有序结构体代替 map。encoding/json 包本身不提供键排序参数,这点和 Python 的 sort_keys 不同。

高性能替代方案

对于 API 网关、日志管道等高吞吐低延迟场景,标准库的反射开销可能成为瓶颈。Go 社区提供了多种加速方案。

方案速度(相对标准库)兼容性
jsoniter运行时优化约 2-3 倍完全兼容标准库 drop-in
sonicJIT + SIMD 汇编约 3-5 倍需 amd64 + Go 1.15+,API 兼容
easyjson代码生成(零反射)约 4-6 倍go generate,struct 固定

选择建议:普通项目直接用标准库;中型 API 服务可考虑 jsoniter(零迁移成本,修改 import 路径即可);对延迟敏感且运行在 amd64 环境用 sonic;编译时结构和数据变化频率低的场景用 easyjson 通过提前生成代码获得最佳性能。

// jsoniter 的 drop-in 替换:只需改 import 别名
import jsoniter "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary

// 后续所有 json.Marshal / json.Unmarshal 调用自动使用 jsoniter
data, _ := json.Marshal(myStruct)

jsoniter 的 ConfigCompatibleWithStandardLibrary 模式提供与标准库几乎完全一致的行为,可以安全地在现有代码库中替换而不需要修改任何业务逻辑代码。sonic 则提供了不兼容标准库的优化路径,需要开发者主动适配其 API。

各语言 JSON API 对比

语言 序列化 反序列化 美化输出 配置方式
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions

常见问题 (FAQ)

JSON 序列化时如何处理日期时间类型?

大多数语言的 JSON 库默认不支持日期时间类型。通常做法是序列化为 ISO 8601 格式字符串(如 "2026-07-15T10:30:00Z")或 Unix 时间戳,反序列化时再转换回日期时间对象。Python 可通过 default 参数指定自定义编码器;JavaScript 可在 toJSON 方法中处理;Java 的 Jackson 支持 @JsonFormat 注解配置日期格式。

如何忽略 JSON 序列化中的 null 值或空字段?

不同语言的实现方式不同:Python 可在自定义编码器中过滤 None 值;JavaScript 可使用 replacer 函数或 JSON.stringify 的第二个参数;Java Jackson 用 @JsonInclude(Include.NON_NULL) 注解;Go 使用 omitempty struct tag;C# 设置 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。

JSON 序列化时如何处理循环引用?

循环引用是 JSON 序列化的常见陷阱。标准 JSON 库通常会抛出异常或栈溢出。解决方案包括:使用 @JsonIdentityInfo 注解(Java Jackson)、实现自定义序列化器跳过反向引用、将对象图转换为 DTO 后再序列化,或使用专门的图序列化库如 flatted(JavaScript)。

为什么我的私有字段没有被序列化?

JSON 序列化库通常只序列化公开(public)字段或带有 getter 的属性。这是设计上的安全考虑。Python 的 json 模块默认只序列化 dict 的公共键;Java Jackson 可通过 @JsonProperty 注解私有字段或配置 ObjectMapper 的 Visibility;C# 中 System.Text.Json 默认只序列化公共属性,需通过 [JsonInclude] 特性包含非公共成员。

JSON 序列化性能有哪些优化技巧?

1) 复用序列化器实例(避免每次创建 ObjectMapper/JsonSerializerOptions);2) 对于大型数据,使用流式 API(如 Jackson 的 JsonGenerator / Python 的 iterencode);3) 使用编译时生成(C# Source Generator、Go easyjson);4) 避免过多嵌套层次;5) 数值类型避免不必要的大精度;6) 使用数据库分页或分块传输大 JSON。

推荐工具

以下工具可以帮助你在 Go 开发中更高效地处理 JSON 数据:

相关文章