[译]Protocol Buffer Basics Go

文章目录

原文链接:Protocol Buffer Basics: Go | Protocol Buffers | Google Developers

本教程使用proto3语言为Go程序员介绍了protocol buffers的基本使用。通过一步步创建一个简单的示例应用,你将会了解到:

  • .proto文件中定义消息格式
  • 使用protocol buffer编译器
  • 使用Go protocol buffer API读写消息

这不是一个使用protocol buffers的完全手册。更多更细节的信息,请参看:Language Guide (proto3)Go API ReferenceGo Generated CodeEncoding Reference

为什么使用protocol buffers?

我们将要创建一个简单的“地址簿”应用,该应用可以从文件中读和写联系人的信息。地址簿中的每一个联系人有名字,ID和联系电话。

你如何序列化和读取这种结构化数据?有这么几种方式可以解决这个问题:

  • 使用gobs序列化Go数据结构。对于Go语言本身而言,这是一个很好的方案,但是如果你需要跨语言平台共享数据,那这种方式并不好
  • 按照你期望的方式把数据编码成单一的字符串,例如4个int类型的数据编码成“12:3:-23:67”。这是一个简单且灵活的办法,但这要求你编写一次性的编码和转换的代码,同时转化代码会带来一定的运行时消耗。对于比较简单的数据,使用这种方式就比较好
  • 将数据序列化成XML。XML对人类可读友好且各种语言都有对应的解析工具使得该方法有点吸引人。XML也能比较好的跨应用/项目共享数据。然而,众所周知,XML占用空间比较大,并且解析XML往往比较耗性能。此外,定位XML DOM树通常比直接访问类字段要复杂的多

Protocol buffers具有灵活、高效、自动化等特点可以解决上述问题。使用Protocol buffer,你需要把数据结构的描述定义在.proto文件中,然后protocol buffer编译器会为你生成一个类,这个类实现了对protocol buffer二进制数据的编码和解析。这个类提供了对各个字段的getter和setter方法用于读写。protocol buffer很重要的一点是,如果将来数据格式扩展了,那么代码中依然可以读取由旧的格式编码的数据。

哪里找示例代码

我们的例子是一系列的命令行程序,这些程序管理了由protocol buffers编码的地址簿数据文件。add_person_go这个命令添加一条记录到数据文件。list_people_go解析数据文件并把结果输出到控制台。

你可以从Github Repo中找到完整的示例:protobuf/examples at master · protocolbuffers/protobuf (github.com)

定义你的protocol格式

创建你的地址簿应用的第一步是定义你的.proto文件。.proto文件中的定义很简单:你为你想要序列化的每一个数据结构添加一个消息,然后为消息中的每个字段指定一个名称和对应的类型。在我们的例子中,在.proto中定义的消息请参考addressbook.proto

.proto文件是从一个包声明开始的,包声明用来防止在不同项目中的名字冲突。

1syntax = "proto3";
2package tutorial;
3
4import "google/protobuf/timestamp.proto";

go_package定义了代码生成所在的路径,包名即路径中的最后一个目录。例如,我们的例子的包名就是”tutorialpb“。

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

接下来,你就可以定义你的消息了。一个消息是包含一组含有类型的字段的集合。许多标准的简单值类型都是支持的,例如boolint32floatdoublestring。你也可以包含其他消息在你要定义的消息里。

 1message Person {
 2  string name = 1;
 3  int32 id = 2;  // Unique ID number for this person.
 4  string email = 3;
 5
 6  enum PhoneType {
 7    MOBILE = 0;
 8    HOME = 1;
 9    WORK = 2;
10  }
11
12  message PhoneNumber {
13    string number = 1;
14    PhoneType type = 2;
15  }
16
17  repeated PhoneNumber phones = 4;
18
19  google.protobuf.Timestamp last_updated = 5;
20}
21
22// Our address book file is just one of these.
23message AddressBook {
24  repeated Person people = 1;
25}

上面的例子中,Person这个消息里包含了PhoneNumber这个消息,AddressBook这个消息包含了Person这个消息。你甚至可以消息里定义另一个消息 - 正如你看到的,PhoneNumber是定义在Person里的。如果你希望你的取值是预先定义的一组值里的一个,那么你可以定义enum类型 - 上面的例子中,你希望电话号码的类型是MOBILEHOME或者 WORK

在每一个字段上诸如"= 1" "= 2"的标记是用于在二进制编码的时候对字段指定一个唯一的标记。相对于更高的标记号码而言,1 -15号在编码的时候占用的空间小于一个字节,因此一个有优化手段是你可以把经常会用到的字段或者重复类型的字段(例如数组)标记为1 - 15,把高于15的留给那些不怎么常用到的字段或者可选的字段。重复类型字段里的每一个元素都需要重新编码标记号码,因此重复类型的字段往往都会用到这种优化 。(这块后面需要再理解一下是什么意思

如果一个字段的值没有设置,那么将使用对应类型的默认值:数字类型的默认值是0,字符串默认值是空字符串,布尔类型的默认值是false。对于嵌套的消息,它的默认值是带有各个字段名的默认值。调用访问器(getter)去读取值的时候,如果对应的字段没有明确赋值,那么就返回对应的默认值。

如果一个字段是重复类型,那么元素的个数可能是任意数目(包括0个)。元素的顺序在protocol buffer中是被保留下来的。可以想象重复类型字段是动态数组。

你将在Protocol Buffer Language Guide中找到如何编写.proto文件的完整教程(包括所有的类型)。至于类似于类继承这种机制就不要找了,protocol buffer不支持。

编译protocol buffers

现在你已经有.proto文件了,接下来你需要基于这个文件去生成访问AddresBook消息(PersonPhoneNumber也是)的类了。你将要使用protocol buffer编译器protoc来编译你的.proto文件:

  1. 如果你还未安装编译器,请先Download Protocol Buffers,然后参考README进行安装

  2. 运行下面的命令安装Go protocol buffers plugin:

    1go get github.com/golang/protobuf/protoc-gen-go
    

编译器plugin protoc-gen-to将被安装在$GOBIN目录,该目录默认是在$GOPATH/bin。这个目录必须包含在你的$PATH中。

  1. 现在你可以运行编译器,指定源目录(就是你的源文件所在的目录,如果没有指定就是指当前目录)和目标目录(生成的代码所在的目录)以及.proto所在的目录。在我们的例子中,你应该运行下面命令:

    1protoc -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结构体包含了NameIdEmailPhones字段
  • Person_PhoneNumber结构体包含了NumberType字段
  • Person_PhoneType结构体和Person.PhoneType枚举

你可以从Go Generated Code guide了解到代码生成的细节,对于大部分代码你只需要像对待Go类型一样。

list_people的单元测试command's unit tests中展示了你应该如何创建Person的实例:

1p := pb.Person{
2        Id:    1234,
3        Name:  "John Doe",
4        Email: "jdoe@example.com",
5        Phones: []*pb.Person_PhoneNumber{
6                {Number: "555-4321", Type: pb.Person_HOME},
7        },
8}

写消息

使用proptocol buffers的目的是序列化你的数据,而后能够在任何地方解析。Go语言中,你可以使用proto库的Marshal方法序列化你的protocol buffer数据。protocol buffer消息的指针类型实现了proto.Message接口。调用proto.Marshal返回序列化后的编码成wire格式的protocol buffer数据。例如,我们在add_person command中使用了这个方法:

 1book := &pb.AddressBook{}
 2// ...
 3
 4// Write the new address book back to disk.
 5out, err := proto.Marshal(book)
 6if err != nil {
 7        log.Fatalln("Failed to encode address book:", err)
 8}
 9if err := ioutil.WriteFile(fname, out, 0644); err != nil {
10        log.Fatalln("Failed to write address book:", err)
11}

读数据

你可以使用proto库中的Unmarshal方法来解析编码过的数据。调用这个方法将buf中的protocol buffer数据解析出来存放在pb中。因此我们使用下面的代码在list_people command中进行解析:

1// Read the existing address book.
2in, err := ioutil.ReadFile(fname)
3if err != nil {
4        log.Fatalln("Error reading file:", err)
5}
6book := &pb.AddressBook{}
7if err := proto.Unmarshal(in, book); err != nil {
8        log.Fatalln("Failed to parse address book:", err)
9}

扩展Protocol Buffer

在你使用protocol buffer的代码发布之后,或早或晚你总是希望改进你的protocol buffer定义。如果你希望你新的定义能向后兼容,且你的老的定义能向前兼容(你一定总是希望这样),那么你需要遵循一些规则。在新版本的protocol buffer中:

  • 你一定不能修改现在的字段的标签号码
  • 你可能会删除某些字段
  • 你可能会新增字段,但是新增的字段一定要使用全新的标签号码(一定是没有被使用过的,包括那些被删除的字段曾经使用过的)。

(使用这些规则有一些例外some exceptions,但这些很少使用到)

如果你遵循了这些规则,你会发现老代码也可以读取新的消息,只是会忽略新的字段。对于老代码,被删除的非数组字段会有它们的默认值,数组类型为空。新的代码可以透明的读取老的消息。

需要注意的是,新的字段不会出现在老消息里,因此你需要添加合理的逻辑处理默认值default value