protobuf 基础语法以及生成Go代码示例

主要内容:protobug基础语法和生成go代码指南。

protobuf的历史

2001年初,Protobuf首先在Google内部创建, 我们把它称之为 proto1,一直以来在Google的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个Google的开发者都会使用到它。

Google开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把Protobuf开源出去。 proto1在演化的过程中有些混乱,所以Protobuf的开发者重写了Protobuf的实现,保留了proto1的大部分设计,以及proto1的很多的想法。但是开源的proto2不依赖任何的Google的库,代码也相当的清晰。2008年7月7日,Protobuf开始公布出来。

Protobuf公布出来也得到了大家的广泛的关注, 逐步地也得到了大家的认可,很多项目也采用Protobuf进行消息的通讯,还有基于Protobuf的微服务框架GRPC。在使用的过程中,大家也提出了很多的意见和建议,Protobuf也在演化,于2016年推出了Proto3。 Proto3简化了proto2的开发,提高了开发的效能,但是也带来了版本不兼容的问题。

目前Protobuf的稳定版本是3.9.2,于2019年9月23日发布。由于很多公司很早的就采用了Protobuf,所以很多项目还在使用proto2协议,目前是proto2和proto3同时在使用的状态。

Protocol Buffer名称来自于初期一个主要的类的名称ProtocolBuffer

Google当前并没有Protobuf的相关专利,所以不必担心侵权的问题。

1.protobuf 基础语法

1.1Message定义

一个message类型定义描述了一个请求或响应的消息格式,可以包含多种类型字段。

例如定义一个搜索请求的消息格式SearchRequest,每个请求包含查询字符串、页码、每页数目。每个字段声明以分号结尾。

syntax = "proto3";
message SearchRequest {
    string query = 1;
    int32  page_number = 2;
    int32  result_per_page = 3;
}
  • 文件的第一行指明了我们使用的是 proto3 语法:若不指定该行 protocol buffer 编译器会认为是 proto2 。该行必须是文件的第一个非空或非注释行。
  • SearchRequest 消息定义了三个字段(名称/值对),字段就是每个要包含在该类型消息中的部分数据。每个字段都具有名称和类型 。每一个字段后面有一个顺次的index,便于消息通信时解析消息内容。

指定字段规则

消息的字段可以是一下规则之一:

  • singular , 格式良好的消息可以有 0 个或 1 个该字段(但不能多于 1 个)。这是 proto3 语法的默认字段规则。
  • repeated ,格式良好的消息中该字段可以重复任意次数(包括 0 次)。重复值的顺序将被保留。

增加更多消息类型

单个 .proto 文件中可以定义多个消息类型。这在定义相关联的多个消息中很有用——例如要定义与搜索消息SearchRequest 相对应的回复消息 SearchResponse,则可以在同一个 .proto 文件中增加它的定义:

essage SearchRequest {
  string query = 1;     //请求消息
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

保留字段名与Tag

可以使用reserved关键字指定保留字段名和标签。注意,不能在一个reserved声明中混合字段名和标签。

message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

.proto文件编译结果

当使用protocol buffer编译器运行.proto文件时,编译器将生成所选语言的代码,用于使用在.proto文件中定义的消息类型、服务接口约定等。不同语言生成的代码格式不同:

  • C++: 每个.proto文件生成一个.h文件和一个.cc文件,每个消息类型对应一个类
  • Java: 生成一个.java文件,同样每个消息对应一个类,同时还有一个特殊的Builder类用于创建消息接口
  • Python: 姿势不太一样,每个.proto文件中的消息类型生成一个含有静态描述符的模块,该模块与一个元类metaclass在运行时创建需要的Python数据访问类
  • Go: 生成一个.pb.go文件,每个消息类型对应一个结构体

基本数据类型

.proto C++ Java Python Go
double double double float float64
float float float float float32
int32 int32 int int int32
int64 int64 long ing/long[3] int64
uint32 uint32 int[1] int/long[3] uint32
uint64 uint64 long[1] int/long[3] uint64
sint32 int32 int intj int32
sint64 int64 long int/long[3] int64
fixed32 uint32 int[1] int uint32
fixed64 uint64 long[1] int/long[3] uint64
sfixed32 int32 int int int32
sfixed64 int64 long int/long[3] int64
bool bool boolean boolean bool
string string String str/unicode[4] string
bytes string ByteString str []byte

默认值

  • 字符串类型默认为空字符串
  • 字节类型默认为空字节
  • 布尔类型默认false
  • 数值类型默认为0值
  • enums类型默认为第一个定义的枚举值,必须是0

导入定义

前面的例子中,我们将 Result 消息定义在了与 SearchResponse 相同的文件中——但若我们需要作为字段类型使用的消息类型已经定义在其他的 .proto 文件中了呢?

可以通过导入操作来使用定义在其他 .proto 文件中的消息定义。在文件的顶部使用 import 语句完成导入其他 .proto 文件中的定义:

import "myproject/other_protos.proto";

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol编译器会根据所选择的不同语言生成服务接口代码。例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
}

生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。

最直接使用 protocal buffer 的 RPC 系统是 gRPC :一款 Google 开源,语言和平台无关的 RPC 系统。gRPC 对 protocol buffer 的支持非常好同时允许使用特定的 protocol buffer 编译器插件来基于 .proto 文件生成相关的代码。

基本规范

1.描述文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

  • 结构定义包括:message、service、enum
  • rpc方法定义结尾的分号可有可无

2.Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式:

message SongServerRequest {
      required string song_name = 1;
  }

3.Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式:

enum Foo {
FIRST_VALUE = 1;
SECOND_VALUE = 2;
}

4.Service名称与RPC方法名统一采用驼峰式命名。

2.生成Go代码指南

主要描述protocol buffer编译器通过给定的.proto会编译生成什么Go代码。

编译器调用

Protobuf核心的工具集是C++语言开发的,官方的protoc编译器中并不支持Go语言,需要安装一个插件才能生成Go代码。用如下命令安装:

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

提供了一个protoc-gen-go二进制文件,当编译器调用时传递了--go_out命令行标志时protoc就会使用它。--go_out告诉编译器把Go源代码写到哪里。编译器会为每个.proto文件生成一个单独的源代码文件。

输出文件的名称是通过获取.proto文件的名称并进行两处更改来计算的:

  • 生成文件的扩展名是.pb.go。比如说player_record.proto编译后会得到player_record.pb.go
  • proto路径(使用--proto_path-I命令行标志指定)将替换为输出路径(使用--go_out标志指定)。

当你运行如下编译命令时:

protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto

编译器会读取文件src/foo.protosrc/bar/baz.proto,这将会生成两个输出文件build/gen/foo.pb.gobuild/gen/bar/baz.pb.go

如果有必要,编译器会自动生成build/gen/bar目录,但是他不能创建build或者build/gen目录,这两个必须是已经存在的目录。

举个例子:

一个简单的消息声明:

message Foo {}

protocol buffer编译器将会生成一个名为Foo的结构体,实现了proto.Message接口的Foo类型的指针。研究了一下工作中的代码,每一个proto文件生成的.go文件中,都包含下面三个方法。

type Foo struct {
}

// 重置proto为默认值
func (m *Foo) Reset()         { *m = Foo{} }

// String 返回proto的字符串表示
func (m *Foo) String() string { return proto.CompactTextString(m) }

// ProtoMessage作为一个tag 确保其他人不会意外的实现
// proto.Message 接口.
func (*Foo) ProtoMessage()    {}

单一message字段

给出如下消息类型

message Bar {}
复制代码

对于一个有Bar类型字段的消息:

// proto3
message Baz {
  Bar foo = 1;
}
复制代码

编译器将会生成一个Go结构体

type Baz struct {
        Foo *Bar
}
复制代码

消息类型的字段可以设置为nil,这意味着该字段未设置,有效清除该字段。这不等同于将值设置为消息结构体的“空”实例。

编译器还生成一个func(m * Baz)GetFoo()* Bar辅助函数。这让不在中间检查nil值进行链式调用成为可能。

可重复字段

每个重复的字段在Go中的结构中生成一个T类型的slice,其中T是字段的元素类型。对于带有重复字段的此消息:

message Baz {
  repeated Bar foo = 1;
}
复制代码

编译器会生成如下结构体:

type Baz struct {
        Foo  []*Bar
}
复制代码

同样,对于字段定义repeated bytes foo = 1;编译器将会生成一个带有类型为[][]byte名为Foo的字段的Go结构体。对于可重复的枚举repeated MyEnum bar = 2;,编译器会生成带有类型为[]MyEnum名为Bar的字段的Go结构体。

 

参考:

1.https://juejin.im/post/5dbaf13ee51d452a0c707f86

2.https://juejin.im/post/5d81bb5cf265da03ae78ab7b

3.https://www.bookstack.cn/read/go-grpc/chapter1-protobuf.md

4.说明文档:https://www.bookstack.cn/read/go-grpc/chapter1-protobuf.md

5.全面protobuf与go:https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/

6.https://halfrost.com/protobuf_encode/

你可能感兴趣的:(基础技术,go语言,protobuf,消息传输)