1. Protobuf 简介
1.1 Protobuf 是什么
Protocol Buffers (简称 Protobuf )是 Google 公司开源的一种轻便高效的结构化数据存储格式,以及用于序列化和反序列化结构化数据的代码生成器。它可以用于通讯协议和数据存储等领域。
Protobuf 是以 .proto 文件形式定义结构化数据的方式和格式。
并且通过代码生成器生成各平台(Java、C++、Python、Go 等)的数据访问类,这些生成的类可以用来在对应的语言中解析、序列化 Protobuf 数据。
1.2 Protobuf 的优点
Protobuf 作为一种数据格式和工具,有以下优点:
(1)性能高,序列化和反序列化速度很快
Protobuf 采用二进制格式存储数据,相比 XML 和 JSON 格式,可以大幅减少数据体积, serialization 和 deserialization 的性能也更优。这对于高性能场景非常有利。
(2)跨平台,多语言支持广泛
Protobuf 提供了标准的 .proto 文件格式和数据描述语法,然后可以通过 protoc 工具,自动生成各主流语言的数据访问类,如 Java、C++、Python、Go 等。
这保证了在不同平台和不同语言 scenarios 下,可以解析和验证一致的 Protobuf 数据。
(3)定义结构化数据格式,方便维护升级
通过 .proto 文件定义数据格式,可以清晰界定不同版本数据格式的兼容关系,格式修改后也方便使用旧格式数据。
(4)数据体积小,便于存储和传输
相比 XML、JSON,Protobuf 的二进制编码可以大幅减小数据体积,节省存储和网络传输成本。
(5)扩展性好,灵活支持新增字段
通过定义可选和 Required 字段,可以轻松添加和删除消息中的字段,而不影响已有字段的序号,便于数据格式的扩展和演进。
2. Go 语言中使用 Protobuf
2.1 在 Go 语言中安装 Protobuf 库
在 Go 语言中使用 Protobuf 主要依赖 google 开源的 golang/protobuf 库,使用以下命令安装:
go get -u github.com/golang/protobuf/proto
安装完成后,可以在代码中 import 此库:
import "github.com/golang/protobuf/proto"
2.2 使用 protoc 编译.proto 文件
编写 .proto 文件后,需要使用 protoc 命令生成 Go 代码,例如:
protoc --go_out=. message.proto
这会根据 message.proto 中的消息定义,生成 Go 语言版本的访问类,存放在 message.pb.go 文件中。
2.3 Protobuf 消息的编码和解码
golang/protobuf 库中主要包含下面两个函数,用来序列化和反序列化 Protobuf 消息:
func Marshal(pb Message) ([]byte, error)func Unmarshal(buf []byte, pb Message) error
其中 Message 是一个满足 protobuf.Message 接口的 Protobuf 消息对象,可以是通过 .proto 生成的 pb.go 文件中定义的类型,也可以是动态消息。
这两个函数可以方便的在任意 Go 类型与 Protobuf 二进制格式之间进行转换。
2.4 Protobuf 服务的定义
除了用于数据存储、网络通信外,Protobuf 也可以用来定义服务接口(RPC 服务)。语法如下:
service SearchService { rpc Search (SearchRequest) returns (SearchResponse);} message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;} message SearchResponse { repeated Result results = 1;} message Result { string url = 1; string title = 2; repeated string snippets = 3;}
这样就定义了一个 RPC 服务 interface,包含一个 Search 方法。然后客户端和服务器端通过实现这个 interface,来发送、处理服务请求和响应。
服务端需要实现服务接口定义的方法,客户端需要调用这个接口方法,传递请求参数,获取响应结果。
3. Protobuf 消息的定义
通过 .proto 文件, 可定义 Protobuf 中的消息结构。Protobuf 消息由一系列字段组成,使用 message 定义,每个消息可包含多种类型的字段。
3.1 消息类型
Protobuf 支持标量类型、复合类型的消息定义。
标量类型: 包括整型、浮点型、布尔型、字符串等;
复合类型: 主要是其他消息类型,一个消息字段可以引用其他消息类型。
3.2 标量类型
语法格式如下:
[修饰符] 类型名 字段名 = 字段号;
常用标量类型和修饰符总结如下:
int32,int64 - 有符号整型
uint32,uint64 - 无符号整型
bool - 布尔类型
string - 字符串类型
bytes - 字节数组
float,double - 浮点类型
repeated - 重复类型,表示数组
required - 必填字段
optional - 可选字段,默认值
示例:
message Person { required string name = 1; required int32 age = 2; optional string email = 3;}
这定义了一个 Person 消息,包含必填的 name、age 字段和可选的 email 字段。
3.3 定义 request 和 response
可定义一对请求和响应消息,用于 RPC 服务接口的输入和输出参数。
语法结构如下:
// SearchRequest请求消息message SearchRequest { required string query = 1; optional int32 page = 2; ...} // SearchResponse响应消息 message SearchResponse { repeated Result results = 1; optional int32 total_results = 2;}
这样通过一对请求响应消息消息,定义了服务接口的入参和返回值格式。
3.4 import 公共 proto 文件
为了重用消息定义和其他 .proto 文件的内容,可以用 import 语句导入其他 .proto 文件。
例如:
import "other/other.proto";
这样就可以直接引用 other.proto 中定义的消息、枚举等。
3.5 使用 options 设置项
Protobuf 支持自定义 options 字段,对消息、枚举进行注解或设置生成参数:
message Foo { optional string text = 1 [(custom_option) = "hello world"]; }
这为 text 字段添加一个自定义 option 注解。
4. Go 语言 Protobuf 实践
下面以一个完整的例子,演示下 Go 语言中使用 Protobuf 的整个流程。
4.1 定义 Protobuf 消息
编写一个 person.proto 文件,定义 Person 消息格式:
syntax = "proto3"; package tutorial; message Person { string name = 1; int32 age = 2; string email = 3;}
4.2 生成 Go 代码
然后使用 protoc 命令根据 person.proto 生成 Go 语言代码:
protoc --go_out=. person.proto
这会生成一个 person.pb.go 文件。
4.3 发送、接收 Protobuf 消息
有了生成的 Go 访问类, 就可很方便的在 Go 代码中处理 Person 消息。
例如序列化和反序列化:
package main import ( "log" "github.com/golang/protobuf/proto" "path/to/personpb" // Import the generated personpb package) func main() { p := &personpb.Person{ Name: "John Doe", Age: 30, Email: "john@email.com", } data, err := proto.Marshal(p) if err != nil { log.Fatal("marshaling error: ", err) } // Handle the marshaled data, for example, print it log.Printf("Marshaled data: %v", data) // If you want to do something with the marshaled data, you can use it here}
4.4 Protobuf 服务端和客户端
可利用 Protobuf 来定义服务接口,下面演示服务器和客户端的实现。
person.proto 中定义服务:
service PersonService { rpc GetPerson(GetPersonRequest) returns (Person) {}} message GetPersonRequest { string name = 1;}
这定义了一个 PersonService,包含获取 Person 的 GetPerson 方法。
在服务器代码中实现这个接口:
type server struct{} func (s *server) GetPerson(ctx context.Context, req *GetPersonRequest) (*Person, error) { // 从数据库中获取Person对象并返回} func main() { lis, err := net.Listen("tcp", ":50051") srv := grpc.NewServer() pb.RegisterPersonServiceServer(srv, &server{}) srv.Serve(lis)}
在客户端, 可调用这个接口:
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())client := pb.NewPersonServiceClient(conn) resp, err := client.GetPerson(context.Background(), &req)
这样通过 gRPC 框架,就可以访问服务器定义的 Protobuf 服务。
4.5 与 gRPC 集成
Protobuf 定义的消息和服务可以很容易的在 gRPC 框架中使用,gRPC 正是通过 Protobuf 接口定义实现服务通信的。
服务器端实现 Protobuf 接口,客户端调用接口,二者通过 gRPC 通讯。
5. Protobuf 使用注意事项和经验
5.1 版本控制
为了兼容旧版本,在修改消息定义时,应该谨慎地创建新字段而不是删除旧字段。
5.2 向后兼容
对于 int32、uint32、int64、uint64、bool、string、bytes 字段,新代码可以读写旧消息,前向后兼容性是没有问题的。
对于 repeated 字段,删除或者顺序改变字段号,会造成不兼容。
新增 optional 或 repeated 字段,前向后兼容性是没有问题。但是新增 required 字段则会造成解析问题。
所以新增字段时,应使用 optional 而不是 required。
5.3 包含大数据量字段
由于 Protobuf 是二进制编码的,如果有字段包含非常大的数据(如图片、视频),会大幅增加消息大小。
这时可以考虑通过指针引用独立文件的形式,避免消息体积过大。
6. 总结
6.1 Protobuf 优缺点
相比 XML 和 JSON 数据格式, Protobuf 作为一种高效的结构化数据存储和交换格式,具有以下优点:
编码效率高,序列化后数据体积小
解析速度快
支持数据格式升级与兼容
支持定义服务接口
跨平台跨语言,通过编译支持各平台访问
当然也存在一些限制,比如不适合处理频繁修改的数据格式,不支持数据查询等。所以 Protobuf 在很多性能敏感、跨平台的场景下,可以发挥很好的作用。
6.2 在 Go 语言项目中的作用
在 Go 语言中,Protobuf 可以用于:
定义项目中的数据结构体
网络服务的请求响应参数和结果
RPC 服务接口定义
数据存储格式定义
通过 Protobuf 接口定义,可以实现服务端和客户端的松耦合。
并且利用 Protobuf 接口语言无关性,可以支持多语言访问后端 Go 服务,实现更好的语言融合。