encoding/json 不该是默认首选,因其在高吞吐低延迟场景下反射解析、字符串重复分配、无类型复用、不支持零拷贝等开销不可忽略,压测中常占CPU 20%+。

为什么 encoding/json 不该是默认首选
它不是慢,而是“在高吞吐、低延迟场景下,开销不可忽略”:反射解析结构体字段、字符串重复分配、无类型复用、不支持零拷贝。压测中常见 json.Marshal 占 CPU 20%+,尤其当 struct 嵌套深、字段多、含 time.Time 或 map[string]interface{} 时更明显。
- 避免在 RPC 响应、消息队列 payload、高频 metrics 打点等路径直接用
json.Marshal/json.Unmarshal -
json.RawMessage可跳过中间解析,但仅适用于透传或延迟解析场景,不能解决序列化本身瓶颈 - 别依赖
json.Marshaler接口做优化——它只是把负担从标准库转移到你自己手上,且无法规避反射和内存分配
什么时候该切到 gogoprotobuf(或 google.golang.org/protobuf)
当你已有明确 schema、服务间通信协议稳定、且能接受 .proto 定义约束时,Protobuf 是当前 Go 生态综合性能最优解。原生 google.golang.org/protobuf 已取代 gogoprotobuf,后者不再维护;但若需更激进的性能(如跳过字段名校验、生成 unsafe 操作),可考虑 github.com/goccy/go-json 配合 proto-like struct tag,或用 github.com/tinylib/msgp。
- 必须启用
marshal_options:设置proto.MarshalOptions{Deterministic: true}保证哈希/缓存一致性 - 避免在 proto message 中混用
oneof和大量 optional 字段——会显著增加 Unmarshal 分支判断开销 - 时间字段统一用
google.protobuf.Timestamp,别用自定义 int64 秒/毫秒字段,否则失去跨语言兼容性和时区安全
msgpack 和 cbor 的真实适用边界
它们适合无固定 schema、但需要比 JSON 更小体积和更快速度的场景,比如日志聚合、设备端配置下发、本地 cache 序列化。但注意:github.com/vmihailenco/msgpack/v5 默认开启反射,要手动关掉;github.com/ugorji/go/codec 支持编译期代码生成(codecgen),能逼近 Protobuf 性能。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
msgpack.Register提前注册 struct 类型,禁用运行时反射:否则和 JSON 差不多慢 -
cbor对浮点数、NaN、负零等语义更严格,调试时容易因NaN != NaN导致 cache 命中失败 - 别用
map[interface{}]interface{}——所有基于 interface{} 的动态类型方案都会触发大量类型断言和分配
自定义二进制序列化:什么情况下值得手写 WriteTo/ReadFrom
只在极少数场景成立:单结构体高频序列化(如每秒百万级 sensor 数据点)、字段全部是基础类型、且 layout 稳定(无新增/删字段)。这时手写可消除所有反射、避免中间 []byte 分配、甚至复用 buffer。
- 必须实现
io.WriterTo和io.ReaderFrom,而非只写MarshalBinary——前者支持 zero-copy write to conn - 用
binary.Write比bytes.Buffer.Write+binary.BigEndian.PutUint64更慢,因为前者有接口调用开销;直接操作[]byteslice + offset 是唯一可控路径 - 版本管理靠前缀 byte:比如首字节写
0x01表 v1,后续升级必须向前兼容,否则线上混布会 panic
真正难的不是选哪个库,而是识别出「哪里真的需要换」。90% 的 HTTP API 并不需要替换 json;但一个 Kafka producer 每秒发 50k 条 struct 消息,就值得为每个 struct 节省 300ns——这背后是 GC 压力下降、P99 延迟收敛、以及机器资源的真实节省。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










