Protobuf 3 入门
1. 什么是序列化?
1.1 概念
序列化(Serialization 或 Marshalling) 是指将数据结构或对象的状态转换成可存储或传输的格式。反向操作称为反序列化(Deserialization 或 Unmarshalling),它的作用是将序列化的数据恢复成原始的数据结构或对象。
简单来说,序列化就像“打包”,反序列化就像“解包”。
2.1 为什么需要序列化?
在计算机系统中,数据通常是以内存中的对象(如 struct、class)形式存在的,而内存数据不能直接在不同程序之间传输,必须先转换成可存储或可传输的格式。序列化的主要用途包括:
- 数据存储:将数据保存到文件、数据库等,例如: - 日志文件
- 配置文件(如 JSON、YAML)
- 持久化存储(如 Redis、MongoDB)
 
- 数据传输:在不同进程或网络之间传输数据,例如: - 前端和后端通信(Web API)
- 微服务之间的通信
- 远程调用(RPC,如 gRPC)
 
- 数据缓存:比如将复杂的对象序列化后存入 Redis,提高访问速度。
- 跨语言兼容:不同编程语言的数据结构不一样,序列化后可以在不同语言之间传输数据。
3.1 序列化的方式
不同的序列化格式适用于不同的应用场景,常见的格式包括:
| 格式 | 特点 | 可读性 | 序列化速度 | 数据大小 | 适用场景 | 
|---|---|---|---|---|---|
| JSON | 文本格式,广泛使用 | 可读 | 适中 | 较大 | Web API,前端后端通信 | 
| XML | 结构化文本,标签冗余 | 可读 | 慢 | 大 | 早期 Web API,配置文件 | 
| YAML | 结构更简洁,适合人阅读 | 可读 | 适中 | 较大 | 配置文件(Kubernetes、Docker) | 
| Protobuf | Google 开发的高效二进制格式 | 不可读 | 快 | 小 | 微服务、gRPC、高性能应用 | 
| MessagePack | 类似 JSON,但体积更小 | 不可读 | 快 | 小 | 移动端、嵌入式系统 | 
| Thrift | Facebook 开发的高效序列化格式 | 不可读 | 快 | 小 | 分布式系统,RPC | 
| Avro | 适用于大数据(如 Hadoop) | 不可读 | 适中 | 小 | 大数据处理 | 
| BSON | MongoDB 的序列化格式 | 不可读 | 适中 | 适中 | MongoDB 数据存储 | 
2. 什么是 Protobuf?
2.1 概念
Protobuf(Protocol Buffers)是 Google 开发的一种高效、跨平台、可扩展的数据序列化协议。它可以将数据转换为紧凑的二进制格式,用于不同系统之间进行高效的数据传输和存储。
简单理解:
- 它类似于 JSON,但比 JSON 体积更小、速度更快。
- 它类似于 XML,但格式更紧凑、解析更高效。
- 它适用于微服务、RPC(远程调用)、数据存储等高性能场景。
2.2 为什么使用 Protobuf?
| 特点 | Protobuf | JSON | XML | 
|---|---|---|---|
| 格式 | 二进制 | 文本 | 文本 | 
| 体积 | 最小 | 较大 | 最大 | 
| 解析速度 | 最快 | 一般 | 最慢 | 
| 可读性 | 不可读 | 可读 | 可读 | 
| 跨语言支持 | 是 | 是 | 是 | 
| 支持 RPC | 是(gRPC) | 否 | 否 | 
如果你的项目涉及:
- 高性能数据通信(微服务、RPC、物联网、游戏服务器)
- 跨语言数据传输(Go、Java、Python、C++、Rust 等)
- 大规模数据存储(日志、数据库、缓存)
那么 Protobuf 是比 JSON、XML 更好的选择。
2.3 Protobuf 的使用场景
- 微服务通信(gRPC) - 适用于 Go、Java、Python、C++ 等语言的微服务之间高效通信。
- 结合 gRPC使用,可以比传统REST API更快。
 
- 数据存储 - 存储日志、缓存数据(如存入 Redis)时,Protobuf 体积小,能节省存储空间。
 
- 跨语言数据交换 - 由于 Protobuf 支持多种编程语言,可以在不同语言的系统之间进行高效数据传输。
 
- 移动端和 IoT(物联网) - 移动端和 IoT 设备通常带宽和存储受限,Protobuf 适用于传输小体积数据,提高性能。
 
3. 简单解释 Protobuf 例子
3.1 Protobuf 文件 simple.proto
 
syntax = "proto3"; // 使用 proto3 语法message SearchRequest {  // 定义一个数据结构(类似 JSON 对象)string query = 1;      // 搜索关键词(字符串)int32 page_number = 2; // 页码(整数)int32 result_per_page = 3; // 每页返回的结果数(整数)
}
解释
- syntax = "proto3";指定使用- proto3语法。
- message SearchRequest定义了一个数据结构(类似 JSON 对象)。
- 每个字段的格式: - 类型(string、int32)
- 字段名称(query、page_number、result_per_page)
- 字段编号(1、2、3,用于唯一标识字段,不能重复)
 
- 类型(
3.2 编译 Protobuf 代码
Protobuf 需要编译后才能用于编程语言(Go、Java、Python 等)。 在终端运行:
protoc --go_out=. simple.proto
- protoc是 Protobuf 编译器
- --go_out=.表示生成 Go 代码,并存放在当前目录
- simple.proto是需要编译的 Protobuf 文件
不同语言对应的参数:
| 语言 | 编译参数 | 
|---|---|
| C++ | --cpp_out=. | 
| Java | --java_out=. | 
| Python | --python_out=. | 
| C# | --csharp_out=. | 
| Rust | --rust_out=. | 
最终会生成 simple.pb.go,这个文件包含 Go 代码,用于操作 SearchRequest 结构。
3.3 生成的 Go 代码
编译后会生成如下 Go 结构:
type SearchRequest struct {Query         string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`PageNumber    int32  `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"`ResultPerPage int32  `protobuf:"varint,3,opt,name=result_per_page,json=resultPerPage,proto3" json:"result_per_page,omitempty"`
}
解释
- SearchRequest是- struct,对应- .proto文件中的- message SearchRequest。
- Query、- PageNumber、- ResultPerPage变量对应- .proto里的字段。
- protobuf:"..."里的信息用于 Protobuf 序列化和解析。
3.4 如何使用这个 Go 结构
package mainimport ("fmt""google.golang.org/protobuf/proto"
)func main() {// 创建 SearchRequest 实例request := &SearchRequest{Query:         "golang protobuf",PageNumber:    1,ResultPerPage: 10,}// **序列化**data, _ := proto.Marshal(request)// **反序列化**newRequest := &SearchRequest{}proto.Unmarshal(data, newRequest)fmt.Println(newRequest) // 输出: {Query:golang protobuf PageNumber:1 ResultPerPage:10}
}
解释
- 创建 SearchRequest结构,并填充数据。
- 使用 proto.Marshal(request)序列化,转换成二进制格式(适合网络传输)。
- 使用 proto.Unmarshal(data, newRequest)反序列化,把二进制恢复成 Go 结构。
4. Protobuf 的数据类型
4.1 标量数据类型(Scalar Types)
Protobuf 提供了一些常见的基本数据类型,对应不同语言的变量类型。
4.1.1 数值类型
| Protobuf 类型 | 说明 | 适用场景 | 
|---|---|---|
| int32 | 32 位整数(默认编码) | 适用于较小的整数 | 
| int64 | 64 位整数(默认编码) | 适用于较大的整数 | 
| uint32 | 无符号 32 位整数 | 适用于只能为正数的情况 | 
| uint64 | 无符号 64 位整数 | 适用于大数且不允许负数 | 
| sint32 | 32 位有符号整数(ZigZag 编码) | 适用于可能包含负数的整数 | 
| sint64 | 64 位有符号整数(ZigZag 编码) | 适用于包含负数的长整数 | 
| fixed32 | 32 位整数(固定长度编码) | 适用于数值分布较均匀的场景 | 
| fixed64 | 64 位整数(固定长度编码) | 适用于较大的定长整数 | 
| sfixed32 | 32 位有符号整数(固定长度编码) | 适用于负数较多的场景 | 
| sfixed64 | 64 位有符号整数(固定长度编码) | 适用于较大的负数 | 
区别:
int32/int64:默认使用 Varint 编码(数据小的时候占用字节更少)。
sint32/sint64:使用 ZigZag 编码,负数编码更高效。
fixed32/fixed64:使用固定长度存储,适合数值分布均匀的情况。
sfixed32/sfixed64:固定长度的有符号整数。
4.1.2 浮点数类型
| Protobuf 类型 | 说明 | 适用场景 | 
|---|---|---|
| float | 32 位浮点数 | 适用于存储小数 | 
| double | 64 位浮点数 | 适用于更高精度的小数 | 
注意:
float占 4 个字节,精度有限。
double占 8 个字节,适用于更高精度计算。
4.1.3 布尔类型
| Protobuf 类型 | 说明 | 适用场景 | 
|---|---|---|
| bool | 布尔值 ( true/false) | 适用于开关、状态等 | 
示例:
message Example {bool is_active = 1; // true or false
}
4.1.4 字符串和字节类型
| Protobuf 类型 | 说明 | 适用场景 | 
|---|---|---|
| string | UTF-8 编码的字符串 | 存储文本信息 | 
| bytes | 原始字节数据 | 适用于存储二进制数据(如文件、图片等) | 
示例:
message Example {string name = 1;bytes file_data = 2;
}
注意:
string只能存储 文本(UTF-8 编码)。
bytes可以存储 任意二进制数据(如图片、视频等)。
4.2 复杂数据类型
4.2.1 数组(Repeated)
使用 repeated 关键字表示 列表/数组:
message Example {repeated string hobbies = 1;repeated int32 scores = 2;
}
- repeated string hobbies = 1;→ 表示字符串数组
- repeated int32 scores = 2;→ 表示整数数组
注意:
- 在 Protobuf 3 中,
repeated类型默认是可选的,不需要额外的optional关键字。
4.2.2 键值对(Map)
Protobuf 3 提供 map<K, V> 类型来存储键值对:
message Example {map<string, int32> scores = 1; // key: string, value: int32
}
- map<string, int32>表示 键为字符串,值为整数 的字典。
- 生成代码后,会转换成 Go 语言的 map[string]int32。
4.2.3 枚举类型(Enum)
enum Status {UNKNOWN = 0;  // 枚举必须从 0 开始ACTIVE = 1;INACTIVE = 2;
}message User {Status status = 1;
}
- enum只能用于定义固定的值(类似- int)。
- 第一个枚举值必须是 0,防止解析错误。
4.2.4 嵌套 Message
message Address {string city = 1;string street = 2;
}message Person {string name = 1;Address address = 2; // 直接嵌套 Address
}
- Person结构里包含- Address结构,可以用于复杂数据存储。
4.3 Protobuf 类型与不同语言的对应关系
| Protobuf 类型 | Go | Java | Python | C++ | 
|---|---|---|---|---|
| int32 | int32 | int | int | int32_t | 
| int64 | int64 | long | int | int64_t | 
| float | float32 | float | float | float | 
| double | float64 | double | float | double | 
| bool | bool | boolean | bool | bool | 
| string | string | String | str | std::string | 
| bytes | []byte | byte[] | bytes | std::string | 
| map<K,V> | map[K]V | Map<K,V> | dict | std::map<K,V> | 
| repeated | []T | List<T> | list | std::vector<T> | 
4.4 Protobuf 3 语法示例
syntax = "proto3";message Person {string name = 1;int32 age = 2;bool is_active = 3;repeated string hobbies = 4;map<string, int32> scores = 5;
}
这个 Person 结构包含:
- string name→ 姓名
- int32 age→ 年龄
- bool is_active→ 是否激活
- repeated string hobbies→ 兴趣爱好(数组)
- map<string, int32> scores→ 课程成绩(键值对)
5. Protobuf 其他字段
5.1 Oneof(互斥字段)
5.1.1 什么是 oneof?
 
oneof 关键字用于定义一组互斥字段,即同一时间只能有一个字段被设置。它的作用类似于 C 语言的 union,但比 union 更智能,可以判断当前设置的是哪个字段。
5.1.2 为什么要用 oneof?
 
在 proto3 版本中,所有字段都有默认值,比如:
message Example {int64 id = 1;
}
- 如果 id没有被设置,默认值是0。
- 但如果 id被显式设置为0,你就无法判断这个0是默认值,还是用户真的设置了0。
oneof 解决了这个问题,因为它提供了一个字段状态检查功能,让你可以判断哪个字段被设置了。
5.1.3 oneof 语法
 
message Response {oneof result {string success_message = 1; // 成功时的消息int32 error_code = 2; // 失败时的错误码}
}
- oneof内的字段是互斥的,最多只能设置一个。
- 如果 success_message被设置,error_code就不能被设置,反之亦然。
- 如果不设置任何字段,oneof字段为空。
适用场景
- API 响应(成功返回
success_message,失败返回error_code)。- 状态表示(例如订单可能是“待支付”或“已完成”,但不能同时处于这两个状态)。
5.1.4 oneof 在 Go 语言中的使用
 
Protobuf 生成的 Go 代码会使用 isXxx() 方法 来判断哪个字段被赋值。
示例:Go 代码
package mainimport ("fmt""google.golang.org/protobuf/proto"
)// 假设 Protobuf 生成的 Go 结构如下:
type Response struct {// 这是 oneof 生成的字段Result isResponse_Result `protobuf_oneof:"result"`
}type isResponse_Result interface {isResponse_Result()
}type Response_SuccessMessage struct {SuccessMessage string
}type Response_ErrorCode struct {ErrorCode int32
}// 实现 isResponse_Result 接口
func (*Response_SuccessMessage) isResponse_Result() {}
func (*Response_ErrorCode) isResponse_Result() {}func main() {// **成功时返回 success_message**resp1 := &Response{Result: &Response_SuccessMessage{SuccessMessage: "Operation successful"}}// **失败时返回 error_code**resp2 := &Response{Result: &Response_ErrorCode{ErrorCode: 404}}// 判断是哪个字段被设置switch v := resp1.Result.(type) {case *Response_SuccessMessage:fmt.Println("Success:", v.SuccessMessage)case *Response_ErrorCode:fmt.Println("Error:", v.ErrorCode)}switch v := resp2.Result.(type) {case *Response_SuccessMessage:fmt.Println("Success:", v.SuccessMessage)case *Response_ErrorCode:fmt.Println("Error:", v.ErrorCode)}
}
输出
Success: Operation successful
Error: 404
- oneof生成了- isResponse_Result接口,允许我们判断哪个字段被设置。
- switch v := resp.Result.(type)语法用于检查当前- oneof字段的类型。
5.1.5 oneof 的应用场景
 
- REST API / gRPC 响应(成功返回数据,失败返回错误码)
- 订单状态(未支付/已支付)
- 用户身份验证(邮箱登录/手机号登录)
- 存储不同类型的数据(文本/图片/视频)
5.2 Reserved(保留字段)
5.2.1 什么是reserved?
 
在 Protobuf 3 中,reserved 关键字用于保留字段编号或名称,防止将来代码演进时误用已删除的字段。
 这可以避免 API 变更时的兼容性问题,确保旧数据不会被错误解析。
5.2.2 为什么需要 reserved?
 
当你删除或修改字段时,如果不使用 reserved,那么:
- 未来新添加的字段可能意外复用旧的字段编号,导致数据解析出错。
- 旧数据仍然可能使用被删除的字段,导致意外行为。
5.2.3 reserved 语法
 
你可以用 reserved 关键字保留字段编号或名称,防止后续被重新使用。
保留字段编号
message User {reserved 2, 4 to 6; // 不能再使用编号 2、4、5、6
}
- 以后不能再使用 2、4、5、6 作为字段编号。
- 如果后续尝试用 field = 2;,编译时会报错。
保留字段名称
message User {reserved "old_name", "deprecated_field"; // 不能再使用这些字段名
}
- 以后不能再使用 “old_name” 和 “deprecated_field” 作为字段名。
同时保留编号和名称
message User {reserved 2, 4 to 6;reserved "old_name", "deprecated_field";
}
- 这样可以同时保留编号和字段名称,防止意外复用。
5.2.4 reserved 作用
 
- 避免旧数据解析错误:如果编号被误用,旧数据可能被错误解析。
- 防止 API 兼容性问题:如果 API 变更,保留字段可以确保旧客户端不会收到无效数据。
- 让代码更可维护:明确告诉后续开发者哪些字段不能使用。
5.3 Any(存储任意数据)
5.3.1 什么是 Any?
 
Any 是 Protobuf 3 提供的一种特殊类型,允许存储任意类型的 Protobuf 消息,适用于动态数据场景。
 它可以在不修改 .proto 结构的情况下,支持不同类型的数据,类似于 JSON 里的 object 或 map<string, any>。
5.3.2 Any 的作用
 
- 存储动态数据:如果一个字段的类型可能变化(例如可能是 User或Order),可以使用Any而不需要改.proto文件。
- 实现灵活的 API 设计:适用于插件系统、事件系统、日志系统,让不同的子系统传递不同的数据结构。
- 避免频繁修改 Protobuf 定义:当不同的客户端需要传输不同的数据类型时,使用 Any可以减少 API 变更的影响。
5.3.3 Any 的基本用法
 
(1)导入 Any 类型
Any 需要导入 google/protobuf/any.proto:
import "google/protobuf/any.proto"; // 引入 Any 类型message Response {string message = 1; google.protobuf.Any data = 2; // 存储任意类型
}
- message是普通字段,存储文本信息。
- data是- Any类型,可以存储任何 Protobuf 消息。
(2)嵌套不同的消息
假设你有两种不同的消息 User 和 Order:
message User {string name = 1;int32 age = 2;
}message Order {int32 order_id = 1;double price = 2;
}message Response {string message = 1;google.protobuf.Any data = 2; // 可以存储 User 或 Order
}
- data字段可以存储- User或- Order,而不需要修改- Response结构。
- 这样,Response可以在不同场景下使用,不受数据类型影响。
5.3.4 Any 在 Go 语言中的使用
 
(1)安装 Protobuf 依赖
在 Go 代码中,需要 proto 和 anypb(处理 Any 类型):
go get google.golang.org/protobuf/proto
go get google.golang.org/protobuf/types/known/anypb
(2)Go 代码示例
package mainimport ("fmt""google.golang.org/protobuf/proto""google.golang.org/protobuf/types/known/anypb"
)// 定义 User 和 Order 结构
type User struct {Name stringAge  int32
}type Order struct {OrderId int32Price   float64
}// Response 结构,包含 Any 字段
type Response struct {Message stringData    *anypb.Any
}func main() {// 创建 User 结构user := &User{Name: "Alice", Age: 25}// 将 User 结构封装到 Any 里anyData, _ := anypb.New(user)// 创建 Response 并存储 User 数据resp := &Response{Message: "User data",Data:    anyData,}// **序列化**data, _ := proto.Marshal(resp)// **反序列化**newResp := &Response{}proto.Unmarshal(data, newResp)// **解析 Any 字段**newUser := &User{}newResp.Data.UnmarshalTo(newUser)fmt.Println("Message:", newResp.Message)fmt.Println("User Name:", newUser.Name, "Age:", newUser.Age)
}
(3)Go 代码解释
- 封装数据: - 使用 anypb.New(user)把User结构转换成Any类型。
 
- 使用 
- 序列化 Response:- 使用 proto.Marshal(resp)进行序列化,便于存储或传输。
 
- 使用 
- 反序列化 Response:- 使用 proto.Unmarshal(data, newResp)解析Response结构。
 
- 使用 
- 解析 Any数据:- newResp.Data.UnmarshalTo(newUser)解析- Any字段,恢复- User结构。
 
6. Protobuf 编码原理
Protobuf 使用高效的二进制格式来存储和传输数据,其中最关键的编码方式之一是 Varint(变长整数编码)。它的核心思想是:
- 数值越小,占用字节越少
- 数值越大,占用字节越多
- 高效存储,减少带宽消耗
6.1 什么是 Varint?
Varint(变长整数编码) 是一种特殊的编码方式,它可以使用 1 到 N 个字节 表示整数。
- 小数占用更少字节(如 1只需要 1 个字节)。
- 大数会自动扩展到多个字节(如 300需要 2 个字节)。
6.2 Varint 编码规则
- 每个字节的最高位(MSB,Most Significant Bit)是“是否还有后续字节的标志” - 最高位为 0:表示这是最后一个字节。
- 最高位为 1:表示后面还有字节。
 
- 最高位为 
- 剩下的 7 位存储数据(低位优先,LSB)。
6.3 具体示例
(1)数字 1 的 Varint 编码
0000 0001  (只有 1 个字节)
- 最高位 0:表示这是最后一个字节。
- 其余 7 位 000 0001(= 1)。
存储方式:
[0000 0001]  → 1 字节
(2)数字 300 的 Varint 编码
先看二进制表示:
300 = 100101100(9 位)
需要拆成 7 位 + 剩余部分:
低 7 位: 0101100  → 0x2C(44)
高 2 位: 0000010  → 0x02(2)
- 第一字节:1010 1100(0xAC)- 最高位 1(表示后面还有字节)。
- 剩余 7位存010 1100(= 44)。
 
- 最高位 
- 第二字节:0000 0010(0x02)- 最高位 0(表示这是最后一个字节)。
- 剩余 7位存000 0010(= 2)。
 
- 最高位 
最终编码
[1010 1100]  [0000 0010]  → 2 字节(0xAC 0x02)
6.4 Wire Type(数据类型编码)
Protobuf 数据存储为 键值对(key-value) 形式,每个字段的 key 也需要编码。
 字段的 key 由字段编号 + Wire Type 组成。
| Wire Type | 值 | 作用 | 
|---|---|---|
| Varint | 0 | 变长整数( int32, int64, bool, enum) | 
| Fixed64 | 1 | 64 位定长( double, fixed64, sfixed64) | 
| Length-delimited | 2 | 变长数据( string, bytes, message, repeated) | 
| Start group | 3 | 已废弃(用于嵌套数据) | 
| End group | 4 | 已废弃 | 
| Fixed32 | 5 | 32 位定长( float, fixed32, sfixed32) | 
存储格式
[字段编号 << 3] | [Wire Type]  [数据]
字段编号左移 3 位,低 3 位存 Wire Type。
6.5 例子:Protobuf 编码解析
假设 Person 结构如下:
message Person {int32 id = 1;        // 1 字段编号string name = 2;     // 2 字段编号
}
数据:
{"id": 150,"name": "Alice"
}
编码过程
-  字段 id = 150- 字段编号 = 1
- Wire Type = 0(Varint)
- key = (1 << 3) | 0 = 0000 1000 (0x08)
- 150的 Varint 编码:- 1001 0110 0000 0001(0x96 0x01)
 最终存储: [0x08] [0x96 0x01] (字段编号 1,Varint)
- 字段编号 = 
-  字段 name = "Alice"- 字段编号 = 2
- Wire Type = 2(Length-delimited,字符串)
- key = (2 << 3) | 2 = 0001 0001 (0x12)
- "Alice"= 5 个字节(- 0x41 0x6C 0x69 0x63 0x65)
 最终存储: [0x12] [0x05] [0x41 0x6C 0x69 0x63 0x65]
- 字段编号 = 
6.6 解析 Protobuf 二进制数据
假设收到如下二进制数据:
08 96 01 12 05 41 6C 69 63 65
逐字节解析:
- 08=- 0000 1000(字段编号- 1,Wire Type- 0,Varint)
- 96 01= 150(Varint 解码)
- 12=- 0001 0010(字段编号- 2,Wire Type- 2,字符串)
- 05= 长度- 5
- 41 6C 69 63 65=- "Alice"
最终解析为:
{"id": 150,"name": "Alice"
}
