从一个荒诞的场景开始
假设你现在要写一个用户登录接口。前端传过来用户名和密码,后端校验通过后,想把用户信息发给订单服务。
你灵机一动:直接把内存里的 User 结构体扔过去不就行了?
go
// 用户服务
user := userService.Login("zhangsan", "123456")
// 直接发给订单服务??
orderService.CreateOrder(user)
如果两个服务在同一个进程里,这确实能跑。但现实中,用户服务在北京机房,订单服务在上海机房,中间隔着几千公里的光纤。你猜会怎样?
订单服务收到的不是"用户信息",而是一串内存地址 ------比如 0xc0000b4008。这个地址在用户服务的内存里指向张三的结构体,但在订单服务的内存里,可能指向一段垃圾数据,或者直接触发段错误(SIGSEGV)。
这就是问题的本质:内存对象的生命周期和空间坐标,极度依赖"当前进程"和"当前机器"。一旦跨出这个边界,它就毫无意义。
什么是序列化与反序列化
既然不能直接传内存对象,我们需要一种"通用语言"------把内存中复杂的结构体图,转换成一段不依赖特定进程、不依赖特定语言、不依赖特定机器 的字节流(或文本流)。这个过程就叫序列化(Serialization)。
反过来,当订单服务收到这串字节流后,再按照约定好的规则,把它还原成自己内存中的结构体。这个过程叫反序列化(Deserialization)。
用一张图概括:
css
[内存结构体] --序列化--> [字节流/文本] --网络传输--> [字节流/文本] --反序列化--> [内存结构体]
Go 光纤/WiFi Go/Rust/Python
一个生活中的类比
想象你要搬家,家里有一张大衣柜(结构体)。你不可能直接把衣柜塞进快递车(网络),而是要先把它拆解成木板和螺丝(序列化) ,打包成标准尺寸的箱子(字节流)。到了新家后,再按照说明书重新组装(反序列化)。
如果少了"拆解"和"说明书",快递车根本不知道怎么运输,新家也不知道怎么还原。
没有序列化,世界会怎样?
为了让你更真切地感受到序列化的必要性,我们来看三个没有它就无法运转的场景。
场景一:网络传输
微服务架构下,服务 A 调用服务 B 是常态。无论用 HTTP 还是 gRPC,数据最终都要变成比特流在网线上跑。
go
// 假设我们有一个简单的订单结构体
type Order struct {
ID int64
Amount float64
Items []Item // 嵌套结构体 + 切片
CreateTime time.Time
}
type Item struct {
ProductID int64
Quantity int
Price float64
}
这个 Order 结构体在内存里的布局是树状的、指针交织的、带有类型元数据的 。网线不认识 time.Time,也不认识 []Item,它只认识 0 和 1。
序列化的作用,就是把这棵树打平成线性的字节序列,并且保留足够的类型信息 和结构信息,让接收方能够无损还原。
场景二:数据持久化
你的应用重启后,用户数据不能丢。但内存是易失的,断电即清空。所以要把结构体存到 Redis、PostgreSQL 或磁盘文件里。
go
// 把结构体存到 Redis
user := User{ID: 1001, Name: "张三"}
data, _ := json.Marshal(user) // 序列化成 JSON 字节流
redisClient.Set(ctx, "user:1001", data, time.Hour)
这行代码的背后,json.Marshal 帮你把 user 序列化成 JSON 字节数组,再写入 Redis。下次读取时,再用 json.Unmarshal 反序列化回来。如果没有这个过程,Redis 里存的就是一串不可读的内存地址。
场景三:进程间通信(IPC)
即使是同一台机器上的两个进程,内存空间也是相互隔离的。进程 A 的 0x1000 地址和进程 B 的 0x1000 地址,物理上可能对应完全不同的内存页。
go
// Go 的 channel 只在同一进程内有效
// 跨进程通信需要借助操作系统机制,比如 Unix Domain Socket
conn, _ := net.Dial("unix", "/tmp/service.sock")
data, _ := json.Marshal(request) // 必须先序列化
conn.Write(data)
操作系统提供的管道、共享内存、Socket 等 IPC 机制,传输的只能是字节数据。结构体必须先"液化"成字节流,才能穿越进程边界。
序列化的四个核心目标
理解了"为什么"之后,我们可以提炼出序列化需要达成的四个目标:
| 目标 | 含义 | 反例 |
|---|---|---|
| 可传输 | 能跨越网络、进程、协程边界 | 直接传内存指针 |
| 可存储 | 能写入磁盘、数据库、缓存,且重启后可读 | 内存断电丢失 |
| 可恢复 | 接收方能准确还原出语义等价的数据结构 | 类型丢失、字段错位 |
| 可互操作 | 不同语言、不同系统能互相理解 | Go 结构体只有 Go 认识 |
注意最后一点:可互操作 。很多时候服务 A 用 Go,服务 B 用 Rust,服务 C 用 Python。序列化协议必须是跨语言的通用契约,而不能是某个语言的私有方言。
反序列化:信任即风险
序列化是把结构体"交出去"的过程,而反序列化则是把外部数据"接进来"的过程。
这里有一个极其重要的认知转折:反序列化不是简单的"逆向工程",它本质上是在你的内存中重建结构体图。如果这段字节流来自不可信的源头,攻击者可以精心构造 payload,让你在反序列化时执行恶意代码。
经典案例:Go 的
encoding/gob虽然相对安全,但如果使用不当的反射机制或第三方库,仍可能引发 panic 或逻辑漏洞。更常见的是 JSON 解析时的资源耗尽攻击(如超大嵌套深度导致栈溢出)。
所以,序列化不仅是技术问题,更是安全问题 。我们在把字节流还原成结构体之前,必须先回答一个问题:我信任这段数据的来源吗?
这个话题我们会在系列第 8 篇《反序列化的安全陷阱》中深挖。现在你只需要记住:序列化是出口,反序列化是入口,入口需要安检。
常见的序列化"方言"
既然要把结构体翻译成通用语言,那"翻译标准"就有很多套。不同的标准在可读性、体积、速度、跨语言性之间做不同的取舍。
这里先给你一个速览,后续篇章会逐一展开:
| 格式 | 类型 | 人类可读 | 跨语言 | 典型场景 |
|---|---|---|---|---|
| JSON | 文本 | ✅ | ✅ | REST API、前后端交互 |
| XML | 文本 | ✅ | ✅ | 企业级配置、SOAP |
| Protobuf | 二进制 | ❌ | ✅ | 微服务 RPC、存储 |
| MessagePack | 二进制 | ❌ | ✅ | 缓存、轻量级通信 |
| gob | 二进制 | ❌ | ❌ | Go 内部高性能序列化 |
没有最好的格式,只有最合适的场景。 这也是本系列的核心主线------不是教你调 API,而是教你在正确的场景做正确的选择。
总结
- 序列化的本质:把进程私有的内存结构体,转换成进程无关的字节流。
- 为什么需要:因为内存结构体无法跨网络、跨进程、跨语言、跨时间(持久化)直接传递。
- 核心目标:可传输、可存储、可恢复、可互操作。
- 安全伏笔:反序列化是危险的入口,信任边界必须明确。