字节笔记本
2026年2月22日
Protobuf 入门指南:Go 语言中的高效数据序列化
本文介绍 Google Protocol Buffer(Protobuf)在 Go 语言中的使用方法和实现原理。Protobuf 是 Google 开源的一种高效、轻便的结构化数据序列化格式,广泛应用于 RPC 通信和数据存储场景。
一、Protobuf 简介
1.1、RPC 通信
对于单独部署、独立运行的微服务实例而言,在业务需要时,需要与其他服务进行通信,这种通信方式是进程之间的通讯方式(inter-process communication,简称 IPC)。
IPC 有两种实现方式,分别为:同步过程调用、异步消息调用。在同步过程调用的具体实现中,有一种实现方式为 RPC 通信方式,远程过程调用(Remote Procedure Call,缩写为 RPC)。
远程过程调用是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。简单地说就是能使应用像调用本地方法一样的调用远程的过程或服务。
1.2、RPC 实现步骤
一个正常的 RPC 过程可以分为以下几个步骤:
- client 调用 client stub,这是一次本地过程调用
- client stub 将参数打包成一个消息,然后发送这个消息(打包过程也叫做 marshalling)
- client 所在的系统将消息发送给 server
- server 的系统将收到的包传给 server stub
- server stub 解包得到参数(解包也被称作 unmarshalling)
- server stub 调用服务过程,返回结果按照相反的步骤传给 client
在上述步骤中,有几个核心问题需要解决:
- Call ID 映射:所有函数都需要有一个自己的 ID,客户端和服务端分别维护一个
{函数 <--> Call ID}的对应表 - 序列化与反序列化:客户端把参数转成字节流传给服务端,服务端再把字节流转成自己能读取的格式
- 网络传输:需要一个网络传输层把 Call ID 和序列化后的参数字节流传递给服务端
1.3、Protobuf 简介
Google Protocol Buffer(简称 Protobuf)是 Google 公司内部的混合语言数据标准,主要用于 RPC 系统和持续数据存储系统。
1.4、Protobuf 应用场景
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化(序列化)。它很适合做数据存储或 RPC 数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
1.5、Protobuf 优点
- 性能好/效率高:Protobuf 以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍
- 代码生成机制:利用编译器根据数据文件自动生成结构体定义和相关方法的文件,极大解放开发者编写数据协议解析过程的时间
- 支持"向后兼容"和"向前兼容":系统升级迭代后仍可处理老版本数据,老版本代码也可处理新类型的数据
- 支持多种编程语言:官方包含 C++、Java、Python,还有多种语言的开源实现
1.6、Protobuf 缺点
- 可读性较差:采用二进制格式编码,开发者无法直接阅读
- 缺乏自描述:必须配备对应的 proto 配置文件才能知道数据结构
二、Protobuf 在 Go 语言中的编程实现
Go 语言中有对应的实现 Protobuf 协议的库,GitHub 地址:https://github.com/golang/protobuf
2.1、环境准备
使用 Go 语言的 Protobuf 库之前,需要相应的环境准备:
-
安装 protobuf 编译器 可以在 https://github.com/protocolbuffers/protobuf/releases 选择适合自己系统的 Proto 编译器程序进行下载并解压
-
配置环境变量 protoc 编译器正常运行需要进行环境变量配置,将 protoc 执行文件所在目录添加到当前系统的环境变量中
2.2、安装
通过如下命令安装 protoc-gen-go 库:
go get github.com/golang/protobuf/protoc-gen-go安装完成以后,protoc-gen-go 可执行文件在本地环境 GOPATH/bin 目录下。
2.3、Protobuf 协议语法
Protobuf 协议的格式
Protobuf 协议规定:使用该协议进行数据序列化和反序列化操作时,首先定义传输数据的格式,并命名为以 .proto 为扩展名的消息定义文件。
message 定义一个消息
假设想定义一个"订单"的消息格式,每一个"订单"都含有一个订单号 ID、订单金额 Num、订单时间 TimeStamp 字段:
message Order{
required string order_id = 1;
required int64 num = 2;
optional int32 timestamp = 3;
}Order 消息格式有 3 个字段,每个字段都有一个名字和一种类型:
- 指定字段类型:字段的类型包括字符串(string)、整形(int32、int64...)、枚举(enum)等数据类型
- 分配标识符:每个字段都有唯一的一个标识符,最小从 1 开始,最大到 536870911,不可以使用 [19000-19999] 的标识号
- 指定字段规则:
required:一个格式良好的消息一定要含有 1 个这种字段optional:消息格式中该字段可以有 0 个或 1 个值repeated:这种字段可以重复任意多次(包括 0 次),相当于 Go 中的 slice
注意:使用 required 弊多于利;在实际开发中更应该使用 optional 和 repeated 而不是 required。
2.4、使用 Protobuf 的步骤
- 创建 .proto 文件
syntax = "proto2";
package example;
message Person {
required string Name = 1;
required int32 Age = 2;
required string From = 3;
}- 编译 .proto 文件,生成 Go 语言文件
protoc --go_out=. test.proto执行后生成对应的 person.pb.go 文件。
- 在程序中使用 Protobuf
package main
import (
"fmt"
"ProtocDemo/example"
"github.com/golang/protobuf/proto"
"os"
)
func main() {
fmt.Println("Hello World. \n")
msg_test := &example.Person{
Name: proto.String("Davie"),
Age: proto.Int(18),
From: proto.String("China"),
}
// 序列化
msgDataEncoding, err := proto.Marshal(msg_test)
if err != nil {
panic(err.Error())
return
}
msgEntity := example.Person{}
err = proto.Unmarshal(msgDataEncoding, &msgEntity)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
return
}
fmt.Printf("姓名:%s\n\n", msgEntity.GetName())
fmt.Printf("年龄:%d\n\n", msgEntity.GetAge())
fmt.Printf("国籍:%s\n\n", msgEntity.GetFrom())
}三、Protobuf 协议语法与原理实现
3.1、Protobuf 协议语法
- message:Protobuf 中定义一个数据结构需要用到关键字 message,类似于 Java 的 class,Go 语言中的 struct
- 标识号:每个字段等号后面都有唯一的标识号,用于在反序列化过程中识别各个字段,范围为 1~2^29 - 1
- 字段规则:required(必须)、optional(可选)、repeated(可重复)
- 数据类型:支持 double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、bool、string 等
常见数据类型映射表:
| .proto 类型 | Java 类型 | C++ 类型 | Go 语言类型 | 备注 |
|---|---|---|---|---|
| double | double | double | float64 | |
| float | float | float | float32 | |
| int32 | int | int | int32 | 可变长编码 |
| int64 | long | int64 | int64 | 可变长编码 |
| uint32 | int | uint32 | uint32 | |
| sint32 | int | int32 | int32 | 编码负数时比 int32 高效 |
| bool | boolean | bool | bool | |
| string | String | String | string |
- 枚举类型:
enum Age{
male=1;
female=2;
}- 字段默认值:
message Address {
required sint32 id = 1 [default = 1];
required string name = 2 [default = '北京'];
optional string pinyin = 3 [default = 'beijing'];
}- message 更新规则:
- 不可以修改已存在域中的标识号
- 所有新增添的域必须是 optional 或者 repeated
- 非 required 域可以被删除,但这些被删除域的标识号不可以再次被使用
- sint32 和 sint64 是相互兼容的
- fixed32 兼容 sfixed32,fixed64 兼容 sfixed64
3.2、Protobuf 序列化原理
3.2.1、Varint
Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。
Varint 中的每个 byte 的最高位 bit 有特殊含义:
- 如果该位为 1,表示后续的 byte 也是该数字的一部分
- 如果该位为 0,则结束
- 其他的 7 个 bit 都用来表示数字
因此小于 128 的数字都可以用一个 byte 表示。
在序列化时,Protobuf 按照 TLV 的格式序列化每一个字段:
- T 即 Tag,也叫 Key
- V 是该字段对应的值 value
- L 是 Value 的长度(如果字段是整形,这个 L 部分会省略)
序列化后的结果就是 KeyValueKeyValue... 依次类推的样式。
Key 的定义如下:
(field_number << 3) | wire_type
Key 由两部分组成:
- 第一部分是 field_number
- 第二部分为 wire_type,表示 Value 的传输类型
参考来源:Golang-100-Days by 千锋教育