Go net/rpc默认用gob吃CPU主因是编码/解码依赖运行时反射、动态处理类型信息、频繁内存分配;实测含20+字段结构体单次Encode触发上百次reflect.Value.Field调用,QPS过千即明显升温。

Go底层RPC协议的CPU损耗主要来自运行时反射、重复内存分配和低效编解码逻辑,不是数据量大本身导致的,而是标准库实现方式决定的。
为什么net/rpc默认用gob会吃CPU
gob在编码/解码时必须动态记录并传输完整类型信息(如包路径、字段名、嵌套结构),每次调用都要走反射路径查找字段偏移、检查可导出性、构造临时map/slice。对一个含20+字段的结构体,单次gob.Encoder.Encode()可能触发上百次reflect.Value.Field()调用。
- pprof中常见高占比函数:
reflect.Value.Interface、encoding/gob.(*Encoder).encodeStruct - 即使结构体只有几百字节,QPS过千后CPU占用就明显上扬
- 不支持跨语言,无法复用已有proto定义或前端JSON Schema
替换gob但保留net/rpc栈的实操要点
若不能立刻迁移到gRPC,可换用msgpack或jsonrpc,但要注意几个硬性约束:
- 必须显式注册codec:
rpc.RegisterCodec(codec.MsgpackSpecRpc),否则服务端收不到请求 - 客户端需调用
client.RegisterCodec(codec.MsgpackSpecRpc, "msgpack"),且codec名要和服务端一致 - 结构体tag必须统一为
codec:"field_name",不能混用json:或protobuf: -
msgpack不校验字段名拼写,字段名错位会导致静默解码为空,建议加单元测试覆盖Encode→Decode往返
json.Unmarshal在RPC热路径里为什么更危险
标准encoding/json比gob还容易失控:它不仅要反射,还要做字符串哈希匹配(字段名→struct字段)、动态类型推断(interface{}分支)、以及大量strconv转换。一旦RPC参数里带map[string]interface{}或[]interface{},性能直接断崖下跌。
- 禁止在handler里写
json.Unmarshal(data, &v interface{}),哪怕只是临时调试 - 改用具体结构体 +
easyjson生成静态方法,v.UnmarshalJSON(data)执行的是纯字段偏移访问 - 如果必须保留泛型接收,至少用
json.RawMessage暂存字节流,等真正需要时再解析,避免“一次RPC、多次反序列化”
真正省CPU的关键不是换协议,而是砍掉反射入口
所有高性能方案(protobuf、flatbuffers、easyjson)本质都靠编译期生成代码绕过reflect包。但容易被忽略的是:只要代码里还存在interface{}、map[string]interface{}、或json.Unmarshal(data, &v)这种泛型签名,runtime就可能 fallback 到反射路径。
哪怕你用了protobuf,如果在中间件里又把proto.Message转成map[string]interface{}做日志,前面所有优化都白搭。最省CPU的做法,是让整个RPC链路从wire到handler全程保持强类型,不碰任何泛型容器。


















