注意:此网站已被弃用。该网站将在 2023 年 1 月 31 日后关闭,而流量将重定向到位于 https://protobuf.dev 的新网站。在此期间,我们仅会针对 protobuf.dev 进行更新。

协议缓冲区基础知识:Go

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本教程简要介绍了 Go 编程人员如何使用协议缓冲区语言的 proto3 版本处理协议缓冲区。通过演示如何创建简单的示例应用,您可以了解如何

  • .proto 文件中定义消息格式。
  • 使用协议缓冲区编译器。
  • 使用 Go 协议缓冲区 API 来写入和读取消息。

这不是一份有关在 Go 中使用协议缓冲区的全方位指南。如需更详细的参考信息,请参阅协议缓冲区语言指南Go API 参考文档Go 生成的代码指南编码参考

为何使用协议缓冲区?

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

如何序列化和检索此类结构化数据?解决此问题的方法有以下几种:

  • 使用 gobs 对 Go 数据结构进行序列化处理。在 Go 特有的环境中,这是一个很好的解决方案,但如果您需要与为其他平台编写的应用共享数据,则这种方法并不理想。
  • 您可以制定临时方式将数据项编码为单个字符串,例如将 4 个整数编码为“12:3:-23:67”。这是一种简单灵活的方法,但确实需要编写一次性编码和解析代码,而且解析过程会产生少量的运行时开销。这最适合编码非常简单的数据。
  • 将数据序列化为 XML。此方法可能极具吸引力,因为 XML 有点通俗易懂,而且针对许多语言都有绑定库。如果您想与其他应用/项目共享数据,这是一个不错的选择。不过,众所周知,XML 需要占用大量空间,编码/解码可能会给应用带来巨大的性能损失。此外,导航 XML DOM 树比在类中浏览简单字段要复杂得多。

协议缓冲区是一种灵活、高效、自动化的解决方案,正好能够解决这一问题。使用协议缓冲区,您可以为要存储的数据结构编写 .proto 说明。然后,协议缓冲区编译器会创建一个类,以实现高效的二进制格式,自动实现和解析协议缓冲区数据。生成的类会为构成协议缓冲区的字段提供 getter 和 setter,并负责以协议形式读取和写入协议缓冲区的详细信息。重要的是,协议缓冲区格式支持逐步扩展该格式,以使代码仍然能够读取使用旧格式编码的数据。

在哪里可以找到示例代码

我们的示例是一组命令行应用,用于管理使用协议缓冲区编码的通讯录数据文件。命令 add_person_go 向数据文件添加新条目。list_people_go 命令会解析数据文件并将数据输出到控制台。

您可以在 GitHub 代码库的示例目录中找到完整的示例。

定义协议格式

如需创建地址簿应用,您需要先创建 .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;

  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;
}

在上面的示例中,Person 消息包含 PhoneNumber 消息,而 AddressBook 消息包含 Person 消息。您甚至可以定义嵌套在其他消息内的消息类型,如您所见,PhoneNumber 类型是在 Person 中定义的。如果您希望字段的某个字段具有预定义的值列表,则还可以定义 enum 类型 - 在这里,您希望指定电话号码可以是 MOBILEHOMEWORK 之一。

每个元素上的“ = 1”和“ = 2”标记用于标识该字段在二进制编码中使用的“标记”。与编号相比,编号 1-15 需要的字节数要少一个,因此为了进行优化,您可以决定将这些标签用于常用元素或重复元素,将标记 16 及更高版本用于不太常用的可选元素。重复字段中的每个元素都需要对代码重新编码,因此重复字段特别适合此优化。

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

如果字段为 repeated,则该字段可以重复任意次数(包括零)。重复值的顺序将在协议缓冲区中保留。将重复字段视为动态大小的数组。

协议缓冲区语言指南中,您可以找到编写 .proto 文件的完整指南,包括所有可能的字段类型。但不要寻找与类继承类似的工具:协议缓冲区不会这样做。

编译协议缓冲区

现在,您已经有了 .proto,接下来需要生成生成 AddressBook(以及 PersonPhoneNumber)消息所需的类。为此,您需要在 .proto 上运行协议缓冲区编译器 protoc

  1. 如果您尚未安装编译器,请下载软件包并按照自述文件中的说明操作。
  2. 运行以下命令以安装 Go 协议缓冲区插件:
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    编译器插件 protoc-gen-go 将安装在 $GOBIN 中,默认为 $GOPATH/bin。它必须位于您的 $PATH 中,协议编译器 protoc 才能找到它。
  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 结构。
  • Person 结构,具有 NameIdEmailPhones 字段。
  • Person_PhoneNumber 结构,具有 NumberType 字段。
  • 类型为 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.Person_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)
}

扩展协议缓冲区

很快或在您发布使用协议缓冲区的代码之后,您肯定会想要“改进”协议缓冲区的定义。如果您希望新缓冲区可向后兼容,而旧缓冲区为向前兼容,并且您几乎肯定希望这样做,则需要遵循一些规则。在新版本的协议缓冲区中:

  • 不得更改任何现有字段的代码编号。
  • 可以删除字段。
  • 可以添加新字段,但必须使用新的标记编号(即,从未在此协议缓冲区中使用的标记编号,即使是已删除的字段也不例外)。

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

如果您遵循这些规则,旧代码会欣然接受新消息,而只会忽略任何新字段。对于旧代码而言,单数字段已被删除,就只有默认值,而已删除的重复字段将为空。新代码还会透明地读取旧消息。

但请注意,新消息不会出现在旧消息中,因此您需要使用默认值执行一些合理的操作。类型专用默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为 false。对于数值类型,默认值为零。