默认json.Marshal因反射开销大且生成文本体积比二进制高2–3倍,导致RPC高频调用时CPU和带宽成为瓶颈,故不宜用于核心消息序列化。

为什么不能直接用 json.Marshal 处理核心 RPC 消息
因为默认 json.Marshal 依赖反射,每次调用都要扫描结构体字段、检查 tag、处理嵌套类型,CPU 开销大;生成的文本体积比二进制格式高 2–3 倍,小消息(
- 嵌套
map[string]interface{}或指针字段会让反射开销翻倍,且无法静态校验字段是否存在 -
json:",omitempty"在反序列化时可能丢失零值字段(如ID: 0),导致业务逻辑误判 - 日志埋点、中间件若对 request body 做
json.Unmarshal,也会意外触发反射路径
Protobuf + gRPC 是当前最稳的深度定制起点
不是“用了 Protobuf 就算定制”,而是从 .proto 文件定义开始控制序列化行为:字段编号决定二进制顺序,optional / repeated 影响编码方式,oneof 生成的是 interface{},必须显式 type assert 才能用——这些全是可干预点。
- 避免用
map<string string></string>,改用repeated KeyValue pairs,否则字段扩展时无法加校验、也无法做 schema diff - 时间字段统一存
int64(UnixMilli),别传time.Time;后者在 Protobuf 中需额外封装,且跨语言时 zone 信息易丢 - 启用
protoc-gen-go-grpc的UseCustomCodec选项后,可替换底层编解码器(比如换成simdjson-go兼容层),但极少需要——99% 场景原生google.golang.org/protobuf已足够快
自定义序列化器的三个硬限制必须守住
手写 WriteUint64 / WriteString 看似极致,但 Go struct 字段顺序变更、新增字段、类型不一致都会直接导致静默解析失败或 panic,没有字段名保护,也没有向后兼容机制。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 字段顺序必须与反序列化端严格一致:
struct{A, B, C}和struct{C, A, B}是两个完全不同的协议 - struct tag(如
json:"id")在自定义序列化中不生效,需手动判断零值是否跳过,omitempty逻辑得重写 - 含未导出字段(首字母小写)的 struct 无法被
gob或手写 encoder 访问,会 panic;而gob还要求所有传输类型提前gob.Register(),漏注册就静默失败
压缩和 buffer 复用是最后 10% 性能的关键
Protobuf 本身不压缩,但小消息(2KB 的消息用 snappy(Go 标准库支持),且仅在 producer 端压缩、consumer 端解压,中间件不参与。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Pool缓存[]byte(如 8KB 预分配切片),避免每次proto.Unmarshal都 malloc - 对高频消息类型,实现
Reset()方法清空字段,并配合sync.Pool复用 struct 实例——注意确保无 goroutine 安全隐患 - Kafka 或 NATS 消息体里不要塞 raw JSON,应统一用 Protobuf wrapper 包一层,把压缩标记(如
is_compressed: true)作为字段而非 header,避免 broker 解析歧义
optional string trace_id = 42;,也要确认所有旧版本 consumer 能安全忽略该字段——这靠的是 Protobuf 的 tag 编号机制,而不是代码里“看起来没改”。

















