Go Protocol Buffer

Protocol Buffer 基础: Go

本教程提供了 Go 程序员使用 protocol buffers 语言 proto3 版本使用 protocol buffers 的基本介绍。通过创建一个简单的示例应用程序,它向您展示了如何

  • .proto 中定义消息格式。
  • 使用 protocol buffer 编译器。
  • 使用 Go protocol buffer API写入和读取消息

这不是一个关于在 Go 中使用 protocol buffers 的全面指南。对于更详细的参考信息, 查看 Protocol Buffer Language Guide, the Go API Reference, the Go Generated Code Guide, and the Encoding Reference.

为什么要使用 protocol buffers?

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

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

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

Protocol buffers 是准确解决此问题的灵活、高效、自动化解决方案。使用 Protocol buffers,您可以编写一个.proto 您希望存储的数据结构的描述。由此,Protocol buffers 编译器创建了一个类,该类以有效的二进制格式实现 Protocol buffers 数据的自动编码和解析。生成的类为组成 Protocol buffers 的字段提供了getter和setter,并将 Protocol buffers 作为一个单元进行读写。重要的是,Protocol buffers 格式支持随时间扩展格式的想法,即代码仍然可以读取用旧格式编码的数据。

在哪可以找到示例代码

我们的示例是一组 command-line 应用程序,用于管理使用 protocol buffers 编码的 address book 数据文件。命令 add_person_go 将新条目添加到 data file。命令 list_people_go 解析 data file 并将数据打印到 console。

示例代码包含在 GitHub 仓库 examples 目录

定义你自己的协议格式

要创建你的 address book 应用程序,你需要从编写一个 .proto 文件开始。.proto 文件的定义是比较简单的:为每一个你需要序列化的数据结构添加一个消息(message),然后为消息(message)中的每一个字段(field)指定一个名字和一个类型。下面就是一个定义你的多个消息(messages)的文件 addressbook.proto.

.proto file以包声明开始,这有助于防止不同项目之间的命名冲突。

syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

go_package option 定义包的导入路径,该路径将包含此文件的所有生成代码。Go包名称将是导入路径的最后一个路径组件。例如,我们的示例将使用包名 “tutorialpb”。

option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";

再往下看,就是若干消息(message)定义了。一个消息就是某些类型的字段的集合。许多标准的、简单的数据类型都可以用作字段类型,包括bool,int32,float,double,以及string。你也可以使用其他的消息(message)类型来作为你的字段类型——在上面的例子中,消息Person就是一个被用作字段类型的例子。

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 类型。在这里,您希望指定电话号码可以是 MOBILE, HOME, or WORK.。

在每一项后面的、类似于“= 1”,“= 2”的标志指出了该字段在二进制编码中使用的唯一“标识(tag)”。标识号1~15编码所需的字节数比更大的标识号使用的字节数要少1个,所以,如果你想寻求优化,可以为经常使用或者重复的项采用1~15的标识(tag),其他经常使用的optional项采用≥16的标识(tag)。在重复的字段中,每一项都要求重编码标识号(tag number),所以重复的字段特别适用于这种优化情况。

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

如果一个 field 是 repeated 的,则该 field 可以重复任意次数(包括零)。重复值的顺序将保留在 protocol buffer 中。将重复 field 视为动态大小的数组。

你可以在 Protocol Buffer Language Guide 一文中找到编写.proto文件的完整指南(包括所有可能的字段类型)。但是,不要想在里面找到与类继承相似的特性,因为 protocol buffers 不是拿来做这个的。

编译你的 protocol buffers

在得到了一个.proto文件之后,下一步你就要生成可以读写AddressBook消息(当然也就包括了 Person 以及PhoneNumber 消息)的类了。此时你需要运行 protocol buffer 编译器来编译你的.proto文件:

  1. 如果你还没有安装该编译器,下载安装包 并参照README文件中的说明来安装。
    1. 运行一下命令安装 Go protocol buffers plugin:
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    

    The compiler plugin

    protoc-gen-go
    

    安装在

    $GOBIN
    

    默认

    $GOPATH/bin
    

    . 必须在你的

    $PATH
    

    protocol 编译

    protoc
    

  1. 现在运行编译器,指定源目录(应用程序源代码所在的目录-如果不提供值,则使用当前目录)、目标目录(您希望生成的代码所在的位置;通常与
    $SRC_DIR
    

    ), and the path to your

    .proto
    

    . 在这种情况下,您将调用:

    protoc -I=SRC_DIR --go_out=DST_DIR $SRC_DIR/addressbook.proto
    

    因为您需要 Go 代码,所以使用

    --go_out
    

    option – 其他支持的语言也提供了类似的选项.

This generates github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go 在指定的目标目录中。

The Protocol Buffer API

生成 addressbook.pb.go 提供了以下有用的类型:

  • An AddressBook 结构 with a People field.
  • A Person 结构 with fields for Name, Id, Email and Phones.
  • A Person_PhoneNumber 结构 , with fields for Number and Type.
  • The type Person_PhoneType and a value defined for each value in the Person.PhoneType enum.

You can read more about the details of exactly what’s generated in the Go Generated Code guide, but for the most part you can treat these as perfectly ordinary Go types.

您可以在 Go Generated Code guide 中阅读更多关于生成的详细信息,但在大多数情况下,您可以将其视为完全普通的 Go 类型。

下面是 list_people command’s unit tests 中的一个示例,说明如何创建 Person 实例:

p := pb.Person{
        Id:    1234,
        Name:  "John Doe",
        Email: "jdoe@example.com",
        Phones: []*pb.Person_PhoneNumber{
                {Number: "555-4321", Type: pb.Person_HOME},
        },
}

写消息

使用 protocol buffers 的全部目的是序列化数据,以便可以在其他地方进行解析。在 Go 中,使用 proto 库的Marshal 函数序列化 protocol buffer 数据。指向 protocol buffer 消息的 struct 的指针实现了 proto.Message 接口”。称之为 proto.Marshal 返回 protocol buffer, 以其 wire 格式编码。例如,我们在 add_person command 中使用此函数

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 函数。调用this将解析 in 中的数据作为 protocol buffer,并将结果放入 book 中。因此,要解析 list_people command 中的文件,我们使用:

// 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向后兼容(backwards-compatible),并且旧的buffer能够向前兼容(forward-compatible)——你一定希望如此——那么你在新的protocol buffer中就要遵守其他的一些规则了:

  • 对已存在的任何字段,你都不能更改其标识(tag)号。

  • 你绝对不能添加或删除任何required的字段。

  • 你可以添加新的optional或repeated的字段,但是你必须使用新的标识(tag)号(例如,在这个protocol buffer中从未使用过的标识号——甚至于已经被删除过的字段使用过的标识号也不行)。

(有一些例外 情况,但是它们很少使用。)

如果你遵守这些规则,老的代码将能很好地解析新的消息(message),并忽略掉任何新的字段。对老代码来说,已经被删除的optional字段将被赋予默认值,已被删除的repeated字段将是空的。新的代码也能够透明地读取旧的消息。但是,请牢记心中:新的optional字段将不会出现在旧的消息中,所以你要么需要显式地检查它们是否由has_前缀的函数置(set)了值,要么在你的.proto文件中,在标识(tag)号的后面用[default = value]提供一个合理的默认值。如果没有为一个optional项指定默认值,那么就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对boolean来说,默认值是false。对数值类型来说,默认值是0。还要注意:如果你添加了一个新的repeated字段,你的新代码将无法告诉你它是否被留空了(被新代码),或者是否从未被置(set)值(被旧代码),这是因为它没有has_标志。