为什么需要序列化?——从内存对象到字节流,理解数据交换的本质

从一个荒诞的场景开始

假设你现在要写一个用户登录接口。前端传过来用户名和密码,后端校验通过后,想把用户信息发给订单服务。

你灵机一动:直接把内存里的 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,而是教你在正确的场景做正确的选择


总结

  1. 序列化的本质:把进程私有的内存结构体,转换成进程无关的字节流。
  2. 为什么需要:因为内存结构体无法跨网络、跨进程、跨语言、跨时间(持久化)直接传递。
  3. 核心目标:可传输、可存储、可恢复、可互操作。
  4. 安全伏笔:反序列化是危险的入口,信任边界必须明确。

相关推荐
代码丰3 小时前
调用多个AI 模型时,如何实现一个简单的熔断机制
后端
Nturmoils3 小时前
3行代码接入!魔珐星云让我3分钟搭出可交互数字人
后端·aigc
Rust语言中文社区3 小时前
【Rust日报】2026-05-24 Secluso v1.0.2 版本发布
开发语言·后端·rust
RainCity3 小时前
Java Swing 自定义组件库分享(九)
java·笔记·后端
掘金者阿豪3 小时前
被一个标量子查询折腾了两天,最后发现是数据库自己“偷了懒”
后端
武子康4 小时前
Java-08 深入浅出 Mybatis 数据库多对多关系设计:中间表、映射与性能优化
java·后端·spring
明月_清风4 小时前
二进制序列化入门——为什么二进制比文本更快、更小?
后端·protobuf·messagepack
咕白m6254 小时前
Excel 工作表名称读取(Python 实现)
后端·python
雪隐4 小时前
AI股票小助手00-导言
人工智能·后端
长安不见5 小时前
从 Codex 的防御式写法说起:Redisson 分布式锁该怎么用
后端