为什么使用 protocol buffer?
我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以在文件中读取和写入人们的联系方式。
地址簿中的每个人都有一个姓名、一个 ID、一个电子邮件地址、一个联系电话号码。
你如何序列化和检索这样的结构化数据?
有几种方法可以解决这个问题:
使用 gobs 序列化 Go 数据结构
这在特定于 Go 的环境中是一个很好的解决方案,但是如果您需要与为其他平台编写的应用程序共享数据,它就不能很好地工作。您可以发明一种特殊方式将数据项编码为单个字符串
例如将 4 个整数编码为“12:3:-23:67”。 这是一种简单而灵活的方法,尽管它确实需要编写一次性的编码和解析代码,并且解析会产生很小的运行时成本。 这最适合编码非常简单的数据。将数据序列化为 XML
这种方法可能非常有吸引力,因为 XML(某种程度)是人类可读的,并且有许多语言的绑定库。 如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。 然而,众所周知,XML 是空间密集型的,对它进行编码/解码会对应用程序造成巨大的性能损失。
protocol buffer 是解决这个问题的灵活、高效、自动化的解决方案。
使用 protocol buffer,您可以编写要存储的数据结构的 .proto 描述
。
由此,protocol buffer 编译器创建了一个类,该类以高效的二进制格式
实现 protocol buffer 数据的自动编码和解析。
生成的类为组成 protocol buffer 的字段提供 getter 和 setter,并将读取和写入 protocol buffer 的细节作为一个单元处理。
重要的是,protocol buffer格式支持随着时间的推移扩展格式的想法,这样代码仍然可以读取用旧格式编码的数据。
示例
https://github.com/protocolbuffers/protobuf/tree/main/examples
定义proto
要创建地址簿应用程序,您需要从 .proto 文件开始。
.proto 文件中的定义很简单:为每个要序列化的数据结构添加一条消息,然后为消息中的每个字段指定名称和类型。
在示例中,定义消息的 .proto 文件是 addressbook.proto。
.proto 文件以包声明开头,这有助于防止不同项目之间的命名冲突。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
go_package 选项定义包的导入路径,该路径将包含此文件的所有生成代码。
Go 包名称将是导入路径的最后一个路径组件。
例如,示例将使用包名“tutorialpb”。
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
接下来,定义 message。 message 是包含一组类型字段的聚合。
许多标准的简单数据类型可用作字段类型,包括 bool、int32、float、double 和 string。还可以通过使用其它 message 类型作为字段类型,来为 message 添加更多结构。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
编译 protocol buffers
现在您已经有了一个 .proto,接下来您需要生成读取和写入 AddressBook(以及由此产生的 Person 和 PhoneNumber)消息所需的类。 为此,您需要在 .proto 上运行protocol buffers 编译器 protoc:
如果您尚未安装编译器,请下载软件包并按照 README 中的说明进行操作。
https://developers.google.cn/protocol-buffers/docs/downloads运行以下命令安装 Go protocol buffers 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
编译器插件 protoc-gen-go 将安装在 $GOBIN 中,默认为 $GOPATH/bin。 它必须在您的 $PATH 中,编译器 protoc 才能找到它。
- 现在运行编译器,指定源目录(应用程序的源代码所在的位置。如果不提供值,则使用当前目录)、目标目录(您希望生成的代码所在的位置;通常与 $SRC_DIR 相同),以及 .proto 的路径。
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
这会在您指定的目标目录中生成 addressbook.pb.go。
The Protocol Buffer API
生成的 addressbook.pb.go 为您提供以下有用的类型:
- AddressBook 结构体,包含 People;
- Person 结构体, 包含 Name, Id, Email, Phones;
- Person_PhoneNumber 结构体, 包含 Number, Type;
- The type Person_PhoneType and a value defined for each value in the Person.PhoneType enum.
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.Person_HOME},
},
}
序列化
使用 protocol buffer 的目的是序列化您的数据,以便可以在其他地方对其进行解析。 在 Go 中,您使用 proto 库的 Marshal
函数来序列化您的协议缓冲区数据。
book := &pb.AddressBook{}
// ...
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
反序列化
要解析编码消息,请使用 proto 库的 Unmarshal 函数。
调用它会将 in 中的数据解析为 protocol buffer 并将结果放入 book 中。
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
扩展 protocol buffer
在你发布使用你的 protocol buffer 的代码之后,迟早会想要“改进” protocol buffer 的定义。 如果您希望新buffer 向后兼容,并且您的旧 buffer 向前兼容。那么您需要遵循一些规则。 在新版本的协议缓冲区中:
- 您不得更改任何现有字段的标签号。
- 您可以删除字段。
- 您可以添加新字段,但必须使用新的标签号(即从未在此 protocol buffer 中使用过的标签号,即使已删除的字段也不使用)。
如果您遵循这些规则,旧代码将愉快地阅读新消息并忽略任何新字段。 对于旧代码,已删除的单个字段将仅具有其默认值,而删除的重复字段将为空。 新代码也将透明地读取旧消息。
但是,请记住,旧消息中不会出现新字段,因此您需要对默认值做一些合理的事情。 使用特定类型的默认值:对于字符串,默认值为空字符串。 对于布尔值,默认值为 false。 对于数字类型,默认值为零。