Protocol Buffer 基础:Go

Go 程序员使用 Protocol Buffers 的入门指南。

本教程为 Go 程序员提供了使用 Protocol Buffers 的入门指南,使用 Protocol Buffers 语言的 proto3 版本。通过逐步创建一个简单的示例应用程序,它将向您展示如何

  • .proto 文件中定义消息格式。
  • 使用 Protocol Buffer 编译器。
  • 使用 Go Protocol Buffer API 编写和读取消息。

这不是使用 Go 中 Protocol Buffers 的全面指南。有关更详细的参考信息,请参阅 Protocol Buffer 语言指南Go API 参考Go 生成代码指南编码参考

问题领域

我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以将人员的联系信息读写到文件和从文件中读取。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话号码。

您如何序列化和检索这样的结构化数据?有几种方法可以解决此问题

  • 使用 gobs 序列化 Go 数据结构。这在 Go 特定环境中是一个很好的解决方案,但如果您需要与为其他平台编写的应用程序共享数据,则效果不佳。
  • 您可以发明一种临时方法将数据项编码为单个字符串,例如将 4 个整数编码为“12:3:-23:67”。这是一种简单灵活的方法,尽管它确实需要编写一次性编码和解析代码,并且解析会带来少量运行时成本。这最适合编码非常简单的数据。
  • 将数据序列化为 XML。这种方法可能非常有吸引力,因为 XML(某种程度上)是人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。但是,XML 非常占用空间,对其进行编码/解码可能会对应用程序造成巨大的性能损失。此外,导航 XML DOM 树比导航类中的简单字段复杂得多。

Protocol Buffers 是解决此类问题的灵活、高效、自动化的解决方案。使用 Protocol Buffers,您可以编写要存储的数据结构的 .proto 描述。由此,Protocol Buffer 编译器会创建一个类,该类使用高效的二进制格式实现 Protocol Buffer 数据的自动编码和解析。生成的类为构成 Protocol Buffer 的字段提供 getter 和 setter,并负责将 Protocol Buffer 作为单元读取和写入的细节。重要的是,Protocol Buffer 格式支持随着时间的推移扩展格式,以便代码仍然可以读取使用旧格式编码的数据。

在哪里找到示例代码

我们的示例是一组用于管理地址簿数据文件的命令行应用程序,这些文件使用 Protocol Buffers 编码。命令 add_person_go 将新条目添加到数据文件。命令 list_people_go 解析数据文件并将数据打印到控制台。

您可以在 GitHub 存储库的 examples 目录 中找到完整的示例。

定义您的协议格式

要创建您的地址簿应用程序,您需要从 .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";

接下来,您有您的消息定义。消息只是一个包含一组类型化字段的聚合。许多标准简单数据类型可用作字段类型,包括 boolint32floatdoublestring。您还可以通过使用其他消息类型作为字段类型来为您的消息添加更多结构。

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_MOBILEPHONE_TYPE_HOMEPHONE_TYPE_WORK 之一。

每个元素上的“= 1”、“= 2”标记标识该字段在二进制编码中使用的唯一“标记”。标记号 1-15 比更高的数字需要少一个字节来编码,因此作为优化,您可以决定将这些标记用于常用或重复的元素,将标记 16 及更高的标记用于不太常用的可选元素。重复字段中的每个元素都需要重新编码标记号,因此重复字段是此优化的特别好的候选者。

如果未设置字段值,则使用 默认值:数字类型为零,字符串为空字符串,布尔值为假。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其所有字段均未设置。调用访问器以获取尚未显式设置的字段的值始终返回该字段的默认值。

如果字段是 repeated,则该字段可以重复任意次数(包括零次)。重复值的顺序将保存在 Protocol Buffer 中。将重复字段视为动态大小的数组。

您将在 Protocol Buffer 语言指南 中找到编写 .proto 文件的完整指南,包括所有可能的字段类型。不过,不要去寻找类似于类继承的功能 - Protocol Buffers 不支持。

编译您的 Protocol Buffers

现在您有了 .proto,接下来您需要做的是生成读取和写入 AddressBook(以及 PersonPhoneNumber)消息所需的类。为此,您需要在您的 .proto 上运行 Protocol Buffer 编译器 protoc

  1. 如果您尚未安装编译器,请 下载软件包 并按照 README 中的说明进行操作。

  2. 运行以下命令安装 Go Protocol Buffers 插件

    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    

    编译器插件protoc-gen-go将安装在$GOBIN中,默认为$GOPATH/bin。协议编译器protoc必须能够找到它,因此它必须在你的$PATH中。

  3. 现在运行编译器,指定源目录(你的应用程序源代码所在的目录 - 如果你不提供值,则使用当前目录)、目标目录(你希望生成的代码存放的位置;通常与$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为你提供了以下有用的类型:

  • 一个包含People字段的AddressBook结构。
  • 一个包含NameIdEmailPhones字段的Person结构。
  • 一个Person_PhoneNumber结构,包含NumberType字段。
  • 类型Person_PhoneType以及在Person.PhoneType枚举中每个值定义的值。

你可以在Go生成代码指南中阅读有关生成内容的更多详细信息,但在大多数情况下,你可以将这些视为完全普通的Go类型。

以下来自list_people命令的单元测试的示例展示了如何创建Person的实例:

p := pb.Person{
    Id:    1234,
    Name:  "John Doe",
    Email: "[email protected]",
    Phones: []*pb.Person_PhoneNumber{
        {Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
    },
}

编写消息

使用协议缓冲区的整个目的是序列化你的数据,以便可以在其他地方解析它。在Go中,你使用proto库的Marshal函数来序列化你的协议缓冲区数据。指向协议缓冲区消息的struct的指针实现了proto.Message接口。调用proto.Marshal将返回以其线格式编码的协议缓冲区。例如,我们在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中的数据解析为协议缓冲区,并将结果放入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

在发布使用协议缓冲区的代码后,迟早你肯定想“改进”协议缓冲区的定义。如果你希望新的缓冲区向后兼容,并且旧的缓冲区向前兼容 - 而你几乎肯定希望这样 - 那么你需要遵循一些规则。在新版本的协议缓冲区中:

  • 不能更改任何现有字段的标签号。
  • 可以删除字段。
  • 可以添加新字段,但必须使用新的标签号(即在此协议缓冲区中从未使用过的标签号,即使是已删除的字段)。

(这些规则有一些例外情况,但很少使用。)

如果你遵循这些规则,旧代码将愉快地读取新消息并简单地忽略任何新字段。对于旧代码,已删除的单一字段将简单地具有其默认值,而已删除的重复字段将为空。新代码也将透明地读取旧消息。

但是,请记住,旧消息中将不存在新字段,因此你需要对默认值做一些合理的操作。将使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为false。对于数字类型,默认值为零。