在Go中使用Protobuf

Posted 网管叨bi叨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在Go中使用Protobuf相关的知识,希望对你有一定的参考价值。


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

  • 在 .proto文件中定义消息格式。

  • 使用protoc编译器编译生成Go代码。

  • 使用Go的protocol buffer API读写消息。

它不是一个全面的在Go中使用protocol buffer的指南,更详细的参考信息请查看前面的两个教程。



为什么使用protocol buffer

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

  • 使用gobs(Go中自定义的序列化编码格式)序列化Go数据结构。这是Go特定环境中的一个很好的解决方案,但如果需要与为其他平台编写的应用程序共享数据,它将无法正常工作。

  • 可以发明一种特殊的方法将数据项编码为单个字符串 - 例如将4个整数编码为“12:3:-23:67”。这是一种简单而灵活的方法,虽然它确实需要编写一次性编码和解析代码,并且解析会产生较小的运行时成本。这最适合编码非常简单的数据。

  • 将数据序列化为XML。这种方法非常有吸引力,因为XML(有点)是人类可读懂的,并且有许多语言都有相应的类库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,XML是众所周知的空间密集型,并且编码/解码它会对应用程序造成巨大的性能损失。此外,导航XML DOM树比通常在类中导航简单字段要复杂得多。

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

获得示例程序

PS: 微信不让加外链点击原文链接去github上下载程序代码吧。

下载这些文件到你的项目目录中:

  • 描述protocol buffer消息格式的 .proto文件 addressbook.proto

  • 命令行程序addperson.go,listpeople.go

定义协议格式

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

 
   
   
 
  1. syntax = "proto3";

  2. package tutorial;


  3. import "google/protobuf/timestamp.proto";

在Go中,protocol buffer的包名称用作Go包,除非您指定了gopackage。即使你确实提供了gopackage,你仍然应该在 .proto文件中定义一个包名,以避免在Protocol Buffers命名空间和非Go语言中发生名称冲突。

接下来,是消息定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可用作字段类型,包括bool,int32,float,double和string。您还可以使用其他消息类型作为字段类型,为消息添加更多结构。

 
   
   
 
  1. message Person {

  2. string name = 1;

  3. int32 id = 2; // Unique ID number for this person.

  4. string email = 3;


  5. enum PhoneType {

  6. MOBILE = 0;

  7. HOME = 1;

  8. WORK = 2;

  9. }


  10. message PhoneNumber {

  11. string number = 1;

  12. PhoneType type = 2;

  13. }


  14. repeated PhoneNumber phones = 4;


  15. google.protobuf.Timestamp last_updated = 5;

  16. }


  17. // Our address book file is just one of these.

  18. message AddressBook {

  19. repeated Person people = 1;

  20. }

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

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

如果一个字段是可重复的,该字段可以重复任意次数(包括零)。重复值的顺序将保留在protocol buffer中。将可重复字段视为变长数组。

您将在Protobuf语言指南中找到编写.proto文件的完整指南 - 包括所有可能的字段类型。不要去寻找类继承类似的东西,protocol buffer不支持这些。

编译protocol buffers

有了 .proto后,你需要做的下一件事是生成你需要读取和写入AddressBook(以及Person和PhoneNumber)消息所需的类(Go中是结构体和结构体方法)。为此,你需要在.proto上运行protocol buffer译器protoc:


  1. 请先确保已经安装了编译器 protoc



  2. protoc需要安装插件才能编译生成Go代码,可以运行如下命令安装插件

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



  3. 现在运行编译器,指定源目录(应用程序的源代码所在的位置 - 如果不提供值,则使用当前目录),目标目录(您希望生成的代码在哪里;通常与$相同) SRC_DIR),以及.proto的路径。在这种情况下,你...:

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



  4. 我们使用的示例go代码中导入编译后的 pb.go文件的路径是 pb"github.com/protocolbuffers/protobuf/examples/tutorial" 所以用protoc编译时使用的目标路径应该是

 
   
   
 
  1. protoc --go_out=$GOPATH/src/github.com/protocolbuffers/protobuf/examples/tutorial ./addressbook.proto

$GOPATH/src/github.com/protocolbuffers/protobuf/examples/tutorial目录需要提前创建好。

Protocol buffer API

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

  • 拥有有People字段的AddressBook结构体。

  • Person_PhoneNumber结构体,包含Number和Type字段。

  • 类型Person_PhoneType和为Person.PhoneType枚举中的每个值定义的常量。

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

行动胜千言,下载教程中提供的代码,运行上面的编译命令,去看看生成的 addressbook.pb.go中的代码吧。

下面是如何创建Person实例的示例:

 
   
   
 
  1. p := pb.Person{

  2. Id: 1234,

  3. Name: "John Doe",

  4. Phones: []*pb.Person_PhoneNumber{

  5. {Number: "555-4321", Type: pb.Person_HOME},

  6. },

  7. }

在Go中序列化protocol buffer数据

使用protocl buffer目的是序列化你的结构化数据,以便可以在其他地方解析它。在Go中,使用 proto库的 Marshal函数来序列化protocol buffer数据。指向消息的结构体的指针实现了 proto.Message接口。调用 proto.Marshal会返回以其有线格式编码的protocol buffer。例如,我们在add_person命令中使用此函数:

 
   
   
 
  1. book := &pb.AddressBook{}

  2. // ...


  3. // Write the new address book back to disk.

  4. out, err := proto.Marshal(book)

  5. if err != nil {

  6. log.Fatalln("Failed to encode address book:", err)

  7. }

  8. if err := ioutil.WriteFile(fname, out, 0644); err != nil {

  9. log.Fatalln("Failed to write address book:", err)

  10. }

在Go中解析protocol buffer

要解析编码消息,请使用 proto库的 Unmarshal函数。调用它将buf中的数据解析为protocol buffer,并将结果放在结构体中。因此,要在list_people命令中解析文件,我们使用:

 
   
   
 
  1. // Read the existing address book.

  2. in, err := ioutil.ReadFile(fname)

  3. if err != nil {

  4. log.Fatalln("Error reading file:", err)

  5. }

  6. book := &pb.AddressBook{}

  7. if err := proto.Unmarshal(in, book); err != nil {

  8. log.Fatalln("Failed to parse address book:", err)

  9. }

运行Go应用程序

  • 命令行中运行 go build add_person.go 和 go build list_people.go 会生成两个二进制文件 add_person和 list_people

  • 命令行运行 ./list_people 程序会从文件 ADDRESS_BOOK读取protocol buffer数据,解析到结构体中然后打印出结构体中的 Person数据。

以上是关于在Go中使用Protobuf的主要内容,如果未能解决你的问题,请参考以下文章

ProtoBuf 生成 Go 代码去掉 JSON tag omitempty

ProtoBuf 生成 Go 代码去掉 JSON tag omitempty

Go分布式缓存 使用 Protobuf 通信(day7)

Go分布式缓存 使用 Protobuf 通信(day7)

Go语言 go get 找不到 google.golang.org/protobuf/encoding/prototext 解决办法

go微服务protobuf中oneofWrapValue和FieldMask的使用