因为json.Marshal生成可读文本,需大量字符串拼接、反射解析和Unicode转义,开销大且体积膨胀;高频场景下成性能瓶颈,应选用紧凑、零拷贝友好、无反射的二进制协议如encoding/binary。
为什么不能直接用 json.Marshal 做高性能序列化
因为 json.marshal 生成的是可读文本,需要大量字符串拼接、类型反射和 unicode 转义,序列化/反序列化开销大,且体积膨胀明显(比如 int64(12345) 序列为 "12345" 占 5 字节,而二进制只需 8 字节)。高频 rpc 或本地缓存场景下,这会成为瓶颈。
真正需要的是紧凑、零拷贝友好、无需反射的二进制协议。Go 标准库的 encoding/binary 是最轻量可靠的起点——它不引入额外依赖,控制粒度细,适合结构体字段已知、布局固定的场景。
- 必须提前知道字段顺序和大小(如
int32总是 4 字节) - 不支持嵌套结构体自动展开,需手动 flatten 或逐字段处理
- 字节序必须显式指定(
binary.BigEndian或binary.LittleEndian),跨平台通信时务必统一
用 binary.Write 写入固定结构体的正确姿势
核心原则:别传指针给 binary.Write,除非你明确想写地址值(那通常不是你想要的)。它要求参数是“可寻址的值”,但更安全的做法是传值或显式取地址。
// 正确:传 &v,v 是值类型
type Header struct {
Version uint16
Length uint32
}
v := Header{Version: 1, Length: 1024}
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, &v) // ✅
// 错误:传 v(值),binary.Write 内部无法取地址,会 panic
binary.Write(buf, binary.BigEndian, v) // ❌ panic: invalid type main.Header
- 所有字段必须是基础类型或数组(如
[32]byte),不能含slice、map、string(它们是头+指针) - 如果结构体含
string,先转成[N]byte再填充,或用binary.Write分段写长度+len 字节内容 - 写入前确保
buf容量足够,否则可能隐式扩容影响性能;可预分配:buf.Grow(int(binary.Size(v)))
如何安全处理变长字段(如字符串、切片)
标准 encoding/binary 不支持变长字段,必须自己约定协议:先写长度,再写内容。关键点在于长度字段自身必须是定长的(如 uint32),且与内容字节序一致。
func WritePacket(w io.Writer, msg string) error {
// 先写字符串长度(4 字节 uint32)
if err := binary.Write(w, binary.BigEndian, uint32(len(msg))); err != nil {
return err
}
// 再写 UTF-8 字节流
_, err := w.Write([]byte(msg))
return err
}
func ReadPacket(r io.Reader) (string, error) {
var length uint32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return "", err
}
buf := make([]byte, length)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
- 避免用
len(s)直接当字节数——如果s是用户输入的字符串,需确认是否 UTF-8 安全;若协议要求字节长度,就用len([]byte(s)) - 读取时务必用
io.ReadFull,而非io.Read,否则可能只读到部分数据 - 长度字段溢出要校验(如
length > 1),防止恶意构造超大 <code>buf导致 OOM
性能陷阱:什么时候该换 gogoprotobuf 或 zstd?
纯 binary 手写协议在字段少、结构稳定时最快,但一旦出现以下情况,手写成本和维护风险会陡增:
- 结构体频繁增删字段,需手动同步读写逻辑和版本兼容处理
- 需要跨语言互通(如 Go ↔ Rust ↔ Python),此时 Protocol Buffers 的 IDL 和生成代码更可靠
- 原始数据本身压缩率高(如日志、传感器采样),单独加一层
zstd或snappy比设计复杂二进制格式更省事
特别注意:不要在 binary.Write 后立刻套 zstd.Encoder 做流式压缩——小包(











