Skip to content
Go
encoding/gob — Go 原生二进制编码

encoding/gob — Go 原生二进制编码

gob 是 Go 专有的二进制序列化格式。它不是跨语言的(不像 JSON、Protobuf),但在 Go 到 Go 的通信中效率很高。


1. 基础用法

type User struct {
    ID   int
    Name string
    Tags []string
}

func basic() {
    var buf bytes.Buffer
    encoder := gob.NewEncoder(&buf)
    decoder := gob.NewDecoder(&buf)

    // 编码
    user := User{ID: 1, Name: "Alice", Tags: []string{"go", "dev"}}
    encoder.Encode(user)

    // 解码
    var decoded User
    decoder.Decode(&decoded)
    fmt.Printf("%+v\n", decoded) // {ID:1 Name:Alice Tags:[go dev]}
}

2. 🔬 接口类型的编码(核心技术)

这是 gob 最强大也最复杂的特性。需要注册具体类型

type Animal interface {
    Speak() string
}

type Dog struct {
    Name string
    Breed string
}

func (d Dog) Speak() string { return "Woof! " + d.Name }

type Cat struct {
    Name string
    Color string
}

func (c Cat) Speak() string { return "Meow! " + c.Name }

func main() {
    // 注册所有可能的实现类型 ← 关键!
    gob.Register(Dog{})
    gob.Register(Cat{})

    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    dec := gob.NewDecoder(&buf)

    // 编码接口切片
    animals := []Animal{
        Dog{Name: "Rex", Breed: "Labrador"},
        Cat{Name: "Whiskers", Color: "Orange"},
    }
    enc.Encode(animals)

    // 解码
    var decoded []Animal
    dec.Decode(&decoded)

    for _, a := range decoded {
        fmt.Println(a.Speak())
    }
}

🚨 陷阱:如果忘记 gob.Register(),解码时会收到 gob: name not registered for interface 错误。这个错误只有在运行时解码时才能发现。


3. Gob vs JSON vs Protobuf 对比

特性 Gob JSON Protobuf
跨语言 ❌ Go only
人类可读 ❌ 二进制 ✅ 文本 ❌ 二进制
数据大小 中等 最小
编解码速度 慢(反射) 最快
接口/多态 ✅ 内建支持 ❌ 需要手写 ❌ 需要 oneof
版本兼容 ✅ 增量字段 ❌ 容易破坏 ✅ 字段编号
场景 Go RPC Web API 微服务间

💡 选择指南

  • 对外 API → JSON
  • Go 服务间高性能通信 → gRPC / Protobuf
  • 简单 Go RPC 不需要跨语言 → Gob
  • 不需要特殊工具链 → Gob(不需要 .proto 文件)

4. Gob 的版本兼容性

Gob 编码是按字段索引位置(而非字段名)来编码的。这意味着它天然支持一定程度的兼容性,但也有一些严格限制。

🔬 编码原理

Gob 在编码结构体时,会记录每个字段的类型和位置索引(0, 1, 2, …),而不是字段名。解码时会按字段类型和索引匹配。

// V1 版本的 Config
type Config struct {
    Port int       // 索引 0
    Host string    // 索引 1
}

// V2 — 在末尾添加字段:兼容 V1 的数据
type Config struct {
    Port     int      // 索引 0 — 与 V1 相同
    Host     string   // 索引 1 — 与 V1 相同
    TLSMode  string   // 索引 2 — V1 数据中没有这个字段
}
// ✅ V1 的数据可以被 V2 解码:TLSMode 得到零值 ""
// ✅ V2 的数据可以被 V1 解码:TLSMode 字段被忽略

✅ 安全操作(兼容)

操作 兼容性 说明
在末尾添加新字段 旧数据解码后新字段为零值
删除末尾字段 编码数据多余部分被忽略
字段类型不变、位置不变 始终兼容

🚨 破坏兼容的操作

操作 后果 说明
删除中间字段 ❌ 解码混乱 后续字段的索引全部偏移
改变字段类型 ❌ 解码失败 类型不匹配导致 error
重新排序字段 ❌ 解码混乱 字段索引改变,数据对应错位
在中间插入字段 ❌ 解码混乱 等同于删除+重新排序
// V1
type Config struct {
    Port int       // 索引 0
    Host string    // 索引 1
}

// ❌ 删除中间字段 — 不兼容!
type Config struct {
    Port     int    // 索引 0 — 正常
    // Host 被删除了!
    TLSMode  string // 索引 1 — 但 V1 数据中索引 1 是 string 类型的 Host!
    // → TLSMode 会错误地接收 Host 的字符串值!
}

// ❌ 改变字段类型 — 不兼容!
type Config struct {
    Port string  // 索引 0 — 但 V1 中 Port 是 int
    Host string  // 索引 1
}
// → 解码报错:类型不匹配

💡 兼容性最佳实践

  1. 新字段始终加在末尾,不要插入或删除中间字段。
  2. 永远不要改变已有字段的类型。如果需要改,添加一个新名字的字段在末尾(如 PortV2),并在代码中做迁移。
  3. 如果必须做破坏性变更,不要再叫 gob,换一种编码方式(如 JSON 或 protobuf),或者将数据重新编码。
  4. gob.Register 的类型名也很关键:如果改变了包路径或类型名,解码会失败。
// 推荐的兼容演进方式
// V1
type Config struct {
    Port int
    Host string
}

// V2 — 安全扩展
type Config struct {
    Port     int
    Host     string
    TLSMode  string `gob:"-"` // 显式跳过 gob(如果不需要持久化)
    // 或者添加新字段后做数据迁移
}

// V3 — 如果必须变更字段类型
type Config struct {
    Port     int
    Host     string
    TLSMode  string
    Timeout  string // 新增字段(V3 新增,末尾)
    // ❌ 不要:把前面的 Port 从 int 改成 string
}

🚨 总结:Gob 的版本兼容性依赖于字段索引的稳定性。如果你不能保证这一点(结构体会频繁重构),建议使用 JSON 或 Protobuf。