gob 是 Go 自己的二进制编码,第一次传输一个类型时会把类型描述一起发过去,后面的消息只带数据。这篇用字节数和耗时验证这个设计:什么时候又小又快,什么时候(每次新建 Encoder/Decoder)反而比 JSON 慢得多;并看它怎么处理字段增减、接口字段。
同一个 Encoder 连续 Encode 4 个 Point{X,Y,Name}:每条消息的字节数 = [51 11 11 11](第一条含类型定义)
每次新建 Encoder:每条消息的字节数 = [51 51 51 51](类型定义每次都重发)
同一个值的 JSON:24 字节({"X":1,"Y":2,"Name":"p"})
全是零值的 Point{} 用新 Encoder 编码:44 字节(零值字段不编码)
第 2 条消息(同一个 Encoder)的十六进制:0aff800104010401017000第一条消息 51 字节,其中大部分是类型定义;之后每条只有 11 字节(JSON 是 24 字节)。每次新建 Encoder 就每条都是 51 字节。所以 gob 适合“一个连接上持续传一串同类型的值”,不适合每条消息单独编码(比如存进数据库的一个字段)。
V1{A:7,B:"hi"} 解码进字段顺序不同、多了 C 的 V2:{B:hi C:0 A:7},err=<nil>
解码进 A 变成 string 的 V3:{A:},err=gob: wrong type (string) for received field V1.A
解码进一个字段名完全不同的结构体:{Z:0},err=gob: type mismatch: no fields matched compiling decoder for Empty接收方按字段名匹配,字段顺序不同、接收方多出来的字段(保持零值)都没问题;字段类型变了(int → string)或者一个字段名都对不上才报错。不过接口字段必须 gob.Register 具体类型(文档说两端都要注册;本实验在同一个进程里,没有把两端分开测)。
没有 Register 就编码含接口字段的结构体:err=gob: type not registered for interface: main.Dog
gob.Register(Dog{}) 之后:编码 err=<nil>,解码 err=<nil>,得到 main.Holder{A:main.Dog{Name:"旺财"}}没注册就编码,报 type not registered for interface: main.Dog;gob.Register(Dog{}) 之后能正常往返。
Big 结构体:JSON 118 字节;gob 新 Encoder 的单条 190 字节;同一个 Encoder 的第 2 条 62 字节 编码:gob 每次新建 Encoder 2163 ns/次、27 次分配;gob 复用 Encoder 347 ns/次、5 次分配;JSON 675 ns/次、7 次分配 解码:gob(每次新建 Decoder)9360 ns/次、200 次分配;JSON 1043 ns/次、8 次分配 解码:一个 Decoder 连续解 1000 条消息,平均每条 619 ns、14 次分配(含第一条编译解码器的成本)
复用 Encoder 编码一个结构体 347 ns,比 JSON 的 675 ns 快,只有 5 次分配;每次新建 Encoder 要 2163 ns、27 次分配,比 JSON 慢。解码的差距更大:每次新建 Decoder 9360 ns、200 次分配,是 JSON(1043 ns)的 9 倍;一个 Decoder 连续解 1000 条平均 619 ns。
The implementation compiles a custom codec for each data type in the stream and is most efficient when a single Encoder is used to transmit a stream of values, amortizing the cost of compilation.
文档说得很直接:它为流里每个类型编译一个定制编解码器,单个 Encoder 传一串值时摊薄了编译成本。