Protocol Buffer 基础:Go面向 Go 程序员的 Protocol Buffer 入门介绍。本教程为 Go 程序员提供了使用 Protocol Buffer 的基础入门,教程使用了 proto3 版本的 Protocol Buffer 语言。通过创建一个简单的示例应用,本教程将向您展示如何:
在 .proto 文件中定义消息格式。使用 Protocol Buffer 编译器。使用 Go Protocol Buffer API 来写入和读取消息。这不是一个在 Go 中使用 Protocol Buffer 的全面指南。要了解更详细的参考信息,请参阅Protocol Buffer 语言指南、Go API 参考、Go 生成代码指南以及编码参考。
问题领域我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以从文件中读取和写入人们的联系方式。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话号码。
你如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题:
使用gobs来序列化 Go 的数据结构。这在纯 Go 环境中是一个不错的解决方案,但如果您需要与其他平台的应用共享数据,它就无法很好地工作。您可以发明一种特殊的方式将数据项编码为单个字符串——例如将 4 个整数编码为“12:3:-23:67”。这是一种简单而灵活的方法,但它需要编写一次性的编码和解析代码,并且解析会带来一些运行时成本。这种方法最适合编码非常简单的数据。将数据序列化为 XML。这种方法可能非常有吸引力,因为 XML(某种程度上)是人类可读的,而且有许多语言的绑定库。如果您想与其他应用/项目共享数据,这可能是一个不错的选择。然而,XML 是出了名的占用空间,并且编码/解码它可能会给应用带来巨大的性能损失。此外,遍历 XML DOM 树比通常情况下遍历类中的简单字段要复杂得多。Protocol Buffer 正是为解决这一问题而设计的灵活、高效、自动化的解决方案。使用 Protocol Buffer,您需要编写一个 .proto 文件来描述您希望存储的数据结构。Protocol Buffer 编译器会根据该文件创建一个类,该类使用高效的二进制格式实现了对 Protocol Buffer 数据的自动编码和解析。生成的类为构成 Protocol Buffer 的字段提供了 getter 和 setter,并负责处理将 Protocol Buffer 作为一个单元进行读写的细节。重要的是,Protocol Buffer 格式支持随着时间的推移扩展格式,使得代码仍然可以读取用旧格式编码的数据。
在哪里找到示例代码我们的示例是一组命令行应用,用于管理一个使用 Protocol Buffer 编码的地址簿数据文件。命令 add_person_go 向数据文件添加一个新条目。命令 list_people_go 解析数据文件并将数据打印到控制台。
您可以在 GitHub 仓库的 examples 目录中找到完整的示例。
定义你的协议格式要创建您的地址簿应用,您需要从一个 .proto 文件开始。.proto 文件中的定义很简单:为您想要序列化的每个数据结构添加一个消息(message),然后为消息中的每个字段指定名称和类型。在我们的示例中,定义消息的 .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";
接下来,是您的消息定义。消息只是一个包含一组类型化字段的聚合体。许多标准的简单数据类型都可以作为字段类型,包括 bool、int32、float、double 和 string。您还可以通过使用其他消息类型作为字段类型,为您的消息添加更多结构。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
在上面的示例中,Person 消息包含 PhoneNumber 消息,而 AddressBook 消息包含 Person 消息。您甚至可以在其他消息内部定义消息类型——如您所见,PhoneNumber 类型是在 Person 内部定义的。如果您希望某个字段的值是预定义列表中的一个,您还可以定义 enum 类型——在这里,您想指定一个电话号码可以是 PHONE_TYPE_MOBILE、PHONE_TYPE_HOME 或 PHONE_TYPE_WORK 中的一种。
每个元素上的 " = 1"、" = 2" 标记标识了该字段在二进制编码中使用的唯一“标签”(tag)。标签号 1-15 比较大的数字少用一个字节来编码,因此作为一种优化,您可以决定将这些标签用于常用或重复的元素,而将标签 16 及以上的数字留给不常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合进行这种优化。
如果某个字段未设置值,则会使用默认值:数字类型为零,字符串为空字符串,布尔值为 false。对于嵌入的消息,默认值始终是该消息的“默认实例”或“原型”,其所有字段都未设置。调用访问器获取未显式设置的字段值时,总是返回该字段的默认值。
如果一个字段是 repeated,该字段可以重复任意次数(包括零次)。重复值的顺序将在协议缓冲区中保留。可以把重复字段看作是动态大小的数组。
您可以在Protocol Buffer 语言指南中找到编写 .proto 文件的完整指南——包括所有可能的字段类型。但不要去寻找类似类继承的功能——Protocol Buffer 不支持这个。
编译你的 Protocol Buffers现在您有了一个 .proto 文件,接下来需要做的就是生成读写 AddressBook(以及 Person 和 PhoneNumber)消息所需的类。为此,您需要在您的 .proto 文件上运行 Protocol Buffer 编译器 protoc:
如果你还没有安装编译器,请下载软件包并按照 README 中的说明进行操作。
运行以下命令来安装 Go Protocol Buffers 插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
编译器插件 protoc-gen-go 将被安装在 $GOBIN 目录中,默认为 $GOPATH/bin。它必须在您的 $PATH 环境变量中,以便 Protocol Buffer 编译器 protoc 能够找到它。
现在运行编译器,指定源目录(您应用程序源代码所在的位置——如果不提供值,则使用当前目录)、目标目录(您希望生成代码存放的位置;通常与 $SRC_DIR 相同),以及您的 .proto 文件的路径。在这种情况下,您将调用:
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
因为您想要 Go 代码,所以使用了 --go_out 选项——其他支持的语言也有类似的选项。
这会在您指定的目标目录中生成 github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go 文件。
Protocol Buffer API生成 addressbook.pb.go 文件会为您提供以下有用的类型:
一个 AddressBook 结构体,带有一个 People 字段。一个 Person 结构体,带有 Name、Id、Email 和 Phones 字段。一个 Person_PhoneNumber 结构体,带有 Number 和 Type 字段。Person_PhoneType 类型,以及为 Person.PhoneType 枚举中的每个值定义的值。您可以在Go 生成代码指南中阅读更多关于具体生成内容的细节,但在大多数情况下,您可以将这些类型视为完全普通的 Go 类型。
以下是来自 list_people 命令的单元测试中的一个示例,展示了如何创建一个 Person 实例:
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
写入消息使用 Protocol Buffer 的全部目的就是序列化您的数据,以便在别处进行解析。在 Go 中,您使用 proto 库的 Marshal 函数来序列化您的 Protocol Buffer 数据。指向 Protocol Buffer 消息 struct 的指针实现了 proto.Message 接口。调用 proto.Marshal 会返回以其线路格式编码的 Protocol Buffer。例如,我们在 add_person 命令中使用了这个函数:
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 中。因此,要在 list_people 命令中解析文件,我们使用:
// 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 的定义。如果您希望新的缓冲区向后兼容,并且旧的缓冲区向前兼容——您几乎肯定希望如此——那么您需要遵守一些规则。在新版本的 Protocol Buffer 中:
您*绝不能*更改任何现有字段的标签号。您*可以*删除字段。您*可以*添加新字段,但必须使用新的标签号(即,在此协议缓冲区中从未使用过的标签号,即使是被删除的字段用过的也不行)。(这些规则有一些例外,但很少使用。)
如果您遵守这些规则,旧代码将能够愉快地读取新消息,并简单地忽略任何新字段。对于旧代码来说,被删除的单数(singular)字段将只有其默认值,而被删除的重复(repeated)字段将为空。新代码也将透明地读取旧消息。
但是,请记住,新字段在旧消息中是不存在的,因此您需要对默认值进行合理的处理。系统会使用特定类型的默认值:对于字符串,默认值是空字符串。对于布尔值,默认值是 false。对于数字类型,默认值是零。