Go与Protocol Buffer,入门级踩坑记录 | 青训营笔记

685 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

本文主要参考Protocol Buffer Basics: Go,只有一些记录+官方手册的部分翻译,不完全遵循原文顺序。

为什么使用Protocol Buffer?

Protocol Buffer可以理解一种序列化结构数据的方案,可以与围绕XML/JSON等格式订制的方案相比,它最主要的特点就是以二进制格式编码,从而实现高效的内容压缩和网络传输。下面是原文翻译:

  • Protocol Buffer是解决这个问题(XML消耗大量空间,XML的编/解码严重降低性能。从DOM树中检索数据非常复杂)的灵活、高效、自动化的解决方案。
  • Protocol Buffer对数据结构的定义是通过.proto格式的描述文件实现的。
  • Protocol Buffer的编译器会创建一个通过高效的二进制格式实现自动编/解码Protocol Buffer数据的类,从而提供Protocol Buffer各字段的getter和setter方法,并处理好相关的读写细节。
  • Protocol Buffer还支持随时间扩展扩展格式的特性,因此对应新格式的代码仍然可以读取用旧格式编码的数据(应该有一些限制)。

安装

  1. 安装Protocol Buffer的编译器protoc Protocol Buffers Github下载页面 需要把/bin文件夹添加到环境变量/系统路径。

  2. 安装go Protocol Buffer插件

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

虽然我挂了梯子,但是这个网址似乎不响应代理链接,直连又连不上,所以不得不临时配了一下goproxy。参考 goproxy.cn/ 里面的描述配置。

.proto文件编写示例

.proto文件文件中的定义相对比较简单,主要用到的语句是message,用message声明需要序列化的数据结构,并给每一个字段指定名称和类型,可以嵌套。示例文件是 addressbook.proto.

// 最新的语法格式应该是proto3
syntax = "proto3";
// .proto文件最好以package声明开头,可以避免项目间的命名冲突。
package tutorial;

// 导入依赖文件
import "google/protobuf/timestamp.proto";

// go_package定义了package的import路径,编译后的类文件会保存在当前目录下的同名文件夹(下面的字符串)。
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";

// message是一组有类型字段的集合。
// bool、int32、float、double和string等类型可用作字段,也可以嵌套message。
message Person {
  // 字段里的"= 1"作为二进制编码当中的唯一标签。
  // 建议采用1-15作为常用或重复字段的编号,而将16及以上的编号用于可选字段。
  string name = 1;
  int32 id = 2;  // 某人的唯一ID
  string email = 3;

  // 枚举类型
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  // 嵌套的message类型
  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;
}

如果没有设置字段的值将会以默认值初始化,数值类型的默认值为0,字符串默认值为空字符串,布尔值为false。嵌套的message类型默认值为"default instance" or "prototype",所有的字段都为空,访问未显示设置的字段会返回字段的默认值。

如果字段声明为repeated类型,则该字段可以重复任意次数(包括零次)。重复值将会依序将保存在Protocol Buffer中。可以将重复字段理解为动态数组。

更详细的内容可以参考Protocol Buffer Language Guide。此外,Protocol Buffer没有类似于类继承的工具。

示例代码编译

相对简单的方式,比如假定main文件夹下存在一个addressbook.proto文件,那么在main文件夹下启动命令行,可以采用如下命令完成编译:

# 确认编译器是否配置好,并查看编译器版本
protoc --version
# 编译命令,输出文件夹为当前目录,待编译的文件为当前目录下的所有以.proto为后缀的文件
protoc --go_out=. *.proto

然后可以在之前定义的go_package github.com/protocolbuffers/protobuf/examples/go/tutorialpb目录下得到一个类似于下列代码的文件:

// versions:
// 这篇文章发布时的最新版本
//     protoc-gen-go v1.28.0
//     protoc        v3.20.1
// source: addressbook.proto
package tutorialpb
// 枚举类型完全是go风格的
var (
   Person_PhoneType_name = map[int32]string{ 0: "MOBILE" }
   Person_PhoneType_value = map[string]int32{ "MOBILE": 0 }
)
// 这里也用到了标签(tag)字符串
type Person struct {
   Id          int32                  `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` // Unique ID number for this person.
   Phones      []*Person_PhoneNumber  `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
   LastUpdated *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
}
// getter方法,但我没找到setter?
func (x *Person) GetId() int32 {
   if x != nil {
      return x.Id
   }
   return 0
}
// 不确定是不是Protocol Buffer根据每个类生成的单独的二进制文件格式
var file_addressbook_proto_rawDesc = []byte{
   0x0a, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, 0x70, 
   0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x75, 0x74, 0x6f, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x1f
}

kitex框架使用protobuf时的小问题

kitex框架同时支持thrift和protobuf两种IDL,其中优先支持thrift。如果需要使用protobuf的话需要通过-type参数进行指定,但是在翻译实例项目的thrift文件到protobuf的时候遇到一个问题,即--experimental_allow_proto3_optional未指定,通过-protobuf参数指定无效,后来发现是proto3标准下默认所有的字段是optional的,所以没有必要再像thrift一样指定哪些字段是optional的。