[关闭]
@phper 2020-12-16T10:12:26.000000Z 字数 21763 阅读 147

probuf学习笔记

以下是目录,正在逐步学习和更新中。

Golang

上一节学习了protoc命令的用法,以及配合生成grpc的相关。这一节,来学习一下proto文件中的语法规则。

protoc 命令通过解析 *.proto文件,来生成对应语言的服务文件。

看一个例子:

  1. syntax = "proto3";
  2. package proto;
  3. option go_package = ".;proto";
  4. message User {
  5. string name=1;
  6. int32 age=2;
  7. }
  8. message Id {
  9. int32 uid=1;
  10. }
  11. //要生成server rpc代码
  12. service ServiceSearch{
  13. rpc SaveUser(User) returns (Id){}
  14. rpc UserInfo(Id) returns (User){}
  15. }

可以看出,它的语法结构有点像Makefile风格。

官网文档

https://developers.google.com/protocol-buffers/docs/proto3

官网文档很全,可以对应的学习。但是很多东西,可能并没有用到,全部学习一边,第一是很累,第二,新人上手,很容易受挫,所以,我直接跳过一些非常基础的,也跳过一些不常用的,本次就学一下常用的一些元素,直接入题。

0. 反人类

protobuf 是一个跨平台的结构数据序列化方法的工具,我们使用之前,得把我们接口里用到的所有数据,在proto文件里先定义出来

这意味着,我们得把接口输入输出的所有的数据字段,都得定义在proto文件里,这一点真的是蛋疼!

比如,我一个接口,吐出的数据是一张表,里面有100个字段,那么,就得先在proto文件里讲这100个字段,用message的方式给定义出来。

这。。。。

所以,难在如何把自己的实际业务接口数据抽象出来,匹配上protobuf的语法,我认为这个是最难的。

1. message

我们初学时,看到的proto文件的例子基本都是从mesage开始的。可以说,它是最基础的,也是90%用的最多的元素了。那么啥是message呢?字面翻译为消息,那么啥是消息呢?

在讲消息之前,我们先回忆一下,普通的接口输出Json格式的数据是啥样的,这样会更好的利于我们理解message:

  1. {
  2. "name": "james",
  3. "age": 18
  4. }

上面是我们调用/UserInfo?id=1接口,输出了用户的信息,是一个json格式的数据,里面有2个参数:姓名和年龄。

由于proto的特性,那么我们就得提前在proto文件里定义一下这2个元素:

  1. syntax = "proto3";
  2. message User {
  3. string name=1;
  4. int32 age=2;
  5. }

我们先不看这一段具体是啥意思,我们先分别用PHP和golang转换输出一下,看下转换后的语言结构是咋样的:

  1. protoc --php_out=:. hello.proto

先看PHP生成后的文件长啥样?它会自动在本目录下生成了2个文件夹:ProtoGPBMetadata

  1. ├── GPBMetadata
  2. └── Hello.php
  3. ├── Proto
  4. └── User.php
  5. ├── hello.proto

Hello.php的内容为:

  1. <?php
  2. # Generated by the protocol buffer compiler. DO NOT EDIT!
  3. # source: hello.proto
  4. namespace GPBMetadata;
  5. class Hello
  6. {
  7. public static $is_initialized = false;
  8. public static function initOnce() {
  9. $pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
  10. if (static::$is_initialized == true) {
  11. return;
  12. }
  13. $pool->internalAddGeneratedFile(hex2bin(
  14. "0a3f0a0b68656c6c6f2e70726f746f120570726f746f22210a0455736572" .
  15. "120c0a046e616d65180120012809120b0a03616765180220012805620670" .
  16. "726f746f33"
  17. ), true);
  18. static::$is_initialized = true;
  19. }
  20. }

咋一眼看,只知道是一个单例模式,具体也不清楚它是啥作用的。不过不要急,继续看User.php文件。

  1. <?php
  2. # Generated by the protocol buffer compiler. DO NOT EDIT!
  3. # source: hello.proto
  4. namespace Proto;
  5. use Google\Protobuf\Internal\GPBType;
  6. use Google\Protobuf\Internal\RepeatedField;
  7. use Google\Protobuf\Internal\GPBUtil;
  8. /**
  9. * Generated from protobuf message <code>proto.User</code>
  10. */
  11. class User extends \Google\Protobuf\Internal\Message
  12. {
  13. /**
  14. * Generated from protobuf field <code>string name = 1;</code>
  15. */
  16. protected $name = '';
  17. /**
  18. * Generated from protobuf field <code>int32 age = 2;</code>
  19. */
  20. protected $age = 0;
  21. /**
  22. * Constructor.
  23. *
  24. * @param array $data {
  25. * Optional. Data for populating the Message object.
  26. *
  27. * @type string $name
  28. * @type int $age
  29. * }
  30. */
  31. public function __construct($data = NULL) {
  32. \GPBMetadata\Hello::initOnce();
  33. parent::__construct($data);
  34. }
  35. /**
  36. * Generated from protobuf field <code>string name = 1;</code>
  37. * @return string
  38. */
  39. public function getName()
  40. {
  41. return $this->name;
  42. }
  43. /**
  44. * Generated from protobuf field <code>string name = 1;</code>
  45. * @param string $var
  46. * @return $this
  47. */
  48. public function setName($var)
  49. {
  50. GPBUtil::checkString($var, True);
  51. $this->name = $var;
  52. return $this;
  53. }
  54. /**
  55. * Generated from protobuf field <code>int32 age = 2;</code>
  56. * @return int
  57. */
  58. public function getAge()
  59. {
  60. return $this->age;
  61. }
  62. /**
  63. * Generated from protobuf field <code>int32 age = 2;</code>
  64. * @param int $var
  65. * @return $this
  66. */
  67. public function setAge($var)
  68. {
  69. GPBUtil::checkInt32($var);
  70. $this->age = $var;
  71. return $this;
  72. }
  73. }

这个php文件里面的生成的几个方法很清晰,方别是采用链式结构设置和读取类的成员变量 age 的值。看来,在PHP里面,protobuf的处理方式是用面向对象的方式来处理数据。1个message,就会生成1个类,这个message里的每一个字段,就会变成php对象里的成员属性。然后再自动生成相同数量的getset方法,来获取和设置每一个成员熟悉的值。嗯。看上去虽然有点繁琐,但是基本也能看的懂。

我们接着看下,golang里面,自动生成的文件是怎样的。

  1. protoc --go_out=:. hello.proto

会在本目录下,生成1个hello.pb.go的文件:

  1. // Code generated by protoc-gen-go. DO NOT EDIT.
  2. // versions:
  3. // protoc-gen-go v1.21.0
  4. // protoc v3.11.3
  5. // source: hello.proto
  6. package proto
  7. import (
  8. proto "github.com/golang/protobuf/proto"
  9. protoreflect "google.golang.org/protobuf/reflect/protoreflect"
  10. protoimpl "google.golang.org/protobuf/runtime/protoimpl"
  11. reflect "reflect"
  12. sync "sync"
  13. )
  14. .....
  15. type User struct {
  16. state protoimpl.MessageState
  17. sizeCache protoimpl.SizeCache
  18. unknownFields protoimpl.UnknownFields
  19. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  20. Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
  21. }
  22. func (x *User) Reset() {
  23. *x = User{}
  24. if protoimpl.UnsafeEnabled {
  25. mi := &file_hello_proto_msgTypes[0]
  26. ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
  27. ms.StoreMessageInfo(mi)
  28. }
  29. }
  30. ......
  31. func (x *User) GetName() string {
  32. if x != nil {
  33. return x.Name
  34. }
  35. return ""
  36. }
  37. func (x *User) GetAge() int32 {
  38. if x != nil {
  39. return x.Age
  40. }
  41. return 0
  42. }
  43. var File_hello_proto protoreflect.FileDescriptor
  44. var file_hello_proto_rawDesc = []byte{
  45. 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70,
  46. 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04,
  47. 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
  48. 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61,
  49. 0x67, 0x65, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
  50. 0x72, 0x6f, 0x74, 0x6f, 0x33,
  51. }
  52. .....

大致看了一眼,乱七八糟的也不知道都是啥意思,但是不要急,看关键的这段就行:

  1. type User struct {
  2. state protoimpl.MessageState
  3. sizeCache protoimpl.SizeCache
  4. unknownFields protoimpl.UnknownFields
  5. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  6. Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
  7. }

我们知道了,golang里面,是以结构体的方式来对接这个message的。message里的每一个字段,都演变成struct里的子元素。这样就可以了。

好了。对于message有1个大致的映像就可以了。我们继续讲message的语法

  1. syntax = "proto3";
  2. message User {
  3. string name=1;
  4. int32 age=2;
  5. }

开头的syntax = "proto3";是来申明这个文件是基于ptoro3语法规则的,有点类似于Python3的文件头部#!/usr/bin/python3申明一样。看了官方文章介绍说,之前也有proto2的版本,protoc解释器默认是proto2的语法。但是,我把proto3改成proto2,执行,却报错了。迷惑不解。

所以,就不纠结是2还是3,直接写3就完事了。但是这一行内容,得写在文件的最开始。

message 后面是消息体的名字, 命名规则是 首字母大写,大驼峰的方式 如果不是大写,转换成go语言的时候,会把自动把首首字母变成大写。转成php语言,不会大小写转换,其他语言暂不清楚。

转换规则:

  1. #转换go
  2. message name_a ===> type nameA struct{...}
  3. message name_A ===> type Name_A struct{...}
  4. message nameA=1 ===> type NameA struct{...}

花括号里面,就是具体的字段了:

  1. string name=1;
  2. int32 age=2;

大致看一下,是先定义字段的类型,是字符型还是整型,这个好理解,可是名字后面的=1,=2 是什么鬼???是说,name 赋值为1,age 赋值为2 吗?

然而,并不是这样,也并没有我们想的这么简单。查看一下官方文档关于这个数字标识:

消息请求结构体中每个字段都有唯一的数字标识,这些标识用来在消息的二进制格式中识别你的字段,并且一旦消息投入使用,这些标识就不应该再被修改。
标识1-15使用一个字节编码,包括数字和字段类型;标识16-2047使用两个字节编码。所以应该保留1-15,用作出现最频繁的消息类型的标识,不要把1-15用完,为以后留点。

所以,对于新手而已,怎么用呢?我的理解就是随便,只要不重复就好了:

  1. message user1 {
  2. string name_a=1;
  3. int32 age_1=2;
  4. string address=5;
  5. int32 gender=3;
  6. }
  7. message user2 {
  8. string name_a=1;
  9. int32 age_1=2;
  10. string address=50;
  11. int32 gender=4;
  12. }

对于新手而已,不要过多的被这个特性给吓唬住了,等你熟练使用了,再去思考他的原理。

message里面的字段的类型有多种,上面列出了2种,string,int32,同时还有这些:

数字类型: double、float、int32、int64、uint32、uint64、sint32、sint64: 存储长度可变的浮点数、整数、无符号整数和有符号整数
存储固定大小的数字类型:fixed32、fixed64、sfixed32、sfixed64: 存储空间固定
布尔类型: bool
字符串: string
bytes: 字节数组
message: 消息类型
enum :枚举类型

这里面的类型和具体编程语言类型的转换和关联是啥样的,可以看官网文档介绍:字段类型定义和转换。我们可以根据我们实际的情况,来定义它们的字段类型。

其中,每一个成员它的命名转换规则,和message名字一样,首字母大写,大驼峰的方式 ,如果不是大写,转换成go语言的时候,会把自动把首首字母变成大写

  1. #转换go
  2. string name_a=1; ===> NameA string
  3. string name_A=1; ===> Name_A string
  4. string nameA=1; ===> NameA string

2. package

我们在定义一个.proto文件时,需要申明这个文件属于哪个包,主要是为了规范整合以及避免重复,这个概念在其他语言中也存在,比如php中的namespace的概念,go中的 package概念。

所以,我们根据实际的分类情况,给每1个proto文件都定义1个包,一般这个包和proto所在的文件夹名子,保持一致。

比如例子中,文件在proto文件夹中,那我们用的package 就为: proto;

3. option

看这个名字,就知道是选项和配置的意思,常见的选项是配置 go_package

  1. option go_package = ".;proto";

现在protoc命令生成go包的时候,如果这一行没加上,会提示错误:

  1. proto git:(master) protoc --go_out=:. hello.proto
  2. 2020/05/21 15:59:40 WARNING: Missing 'go_package' option in "hello.proto", please specify:
  3. option go_package = ".;proto";
  4. A future release of protoc-gen-go will require this be specified.
  5. See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.

所以,这个go_package和上面那个package proto;有啥区别呢?有点蒙啊。

我尝试这样改一下:

  1. syntax = "proto3";
  2. package protoB;
  3. option go_package = ".;protoA";

我看下,生成的go语言包的package到底是啥?打开,生成后的go文件:

  1. # vi hello.pb.go
  2. package protoA
  3. ...

发现是protoA,说明,go的package是受option go_package影响的。所以,在我们没有申请这一句的时候,系统就会用proto文件的package名字,为提示,让你也加上相同的go_package名字了。

再来看一下,这个=".;proto" 是啥意思。我改一下:

  1. option go_package = "./protoA";

执行后,发现,生成了一个protoA文件夹。里面的hello.pb.go文件的package也是protoA。

所以,.;表示的是就在本目录下的意思么???行吧。

再来看一下,我们改成1个绝对的路径目录:

  1. option go_package = "/";

所以,总结一下:

  1. package protoB; //这个用来设定proto文件自身的package
  2. option go_package = ".;protoA"; //这个用来生成的go文件package。一般情况下,可以把这2个设置成一样

option也可以用来定义一些常量。

4. 注释

和其他编程语言变成一样,proto的注释支持单行注释合多行注释。比如:

  1. /* SearchRequest represents a search query, with pagination options to
  2. * indicate which results to include in the response.
  3. */
  4. message SearchRequest {
  5. string query = 1;
  6. int32 page_number = 2; // Which page number do we want?
  7. int32 result_per_page = 3; // Number of results to return per page.
  8. }

5. 保留字 reserved

保留字段用关键字reserved来申请,表示这个字段名或者数字标识被保留了,不能在message中被使用。有点像编程语言中的关键字,比如"if else while"这些都是保留字,不能在代码中自己申明使用。我们看个例子:

  1. syntax = "proto3";
  2. package protoA;
  3. option go_package = ".;protoA";
  4. message searchRequest {
  5. reserved 2, 15, 9 to 11;
  6. reserved "query", "bar";
  7. string query = 1;
  8. int32 page_number = 2;
  9. int32 result_per_page = 3;
  10. repeated string snippets = 9;
  11. }

是啥意思呢?表示数字编号:2,15,9到11,这5个数字,你不能再使用了,同样,还有字段名: "query", "bar" 这2个,你也不能再使用了。值得注意的是,这个限制的生效范围是本message内。其他message中如果出现相同的数字编号和字段名,是不受影响的。

比如:

  1. message searchRequest {
  2. reserved 2, 15, 9 to 11;
  3. reserved "query", "bar";
  4. int32 result_per_page = 3;
  5. }
  6. message searchRequest2 {
  7. string query = 1;
  8. int32 page_number = 2;
  9. int32 result_per_page = 3;
  10. repeated string snippets = 9;
  11. }

那么,我们设定了保留字段后,我们要么就删除掉违规的字段名,要么注释点,不然,编辑器会红点报错,你编译的时候也会提示报错信息:

  1. $ protoc --go_out=. emeu.proto
  2. emeu.proto:10:12: Field name "query" is reserved.
  3. emeu.proto: Field "page_number" uses reserved number 2.
  4. emeu.proto: Field "snippets" uses reserved number 9.

所以我们可以直接删掉,或者注释掉:

  1. message searchRequest {
  2. reserved 2, 15, 9 to 11;
  3. reserved "query", "bar";
  4. //string query = 1;
  5. //int32 page_number = 2;
  6. int32 result_per_page = 3;
  7. //repeated string snippets = 9;
  8. }

这样就不会报错了。

可能你会想,这不是多此一举么。这样看似很傻的使用场景是什么呢?答辩是:规避风险,因为我们的ptoro可能是被其他人调用的,也可能还会被团队里的其他项目迭代修改的,我自己用我肯定会准守这个保留字,如果是其他项目其他人用呢?其他人就不一定能准守了。

6. 枚举enum

在ptoro里面也是支持枚举的,那么啥是枚举呢?简单来说从我给定的1个固定数据的集合里来取值。用的比较多的就是月份和星期几,这些都是固定的数据,我们想列出今天是几月今天是星期几,都是从这个大池子里取数的。

比如,星期几,就可以用枚举来定义一个Day的枚举类型

  1. enum Day {
  2. MON = 0;
  3. TUE = 1;
  4. WED = 2;
  5. THU = 3;
  6. FRI = 4;
  7. SAT = 5;
  8. SUN = 6;
  9. }

值得注意的是,刚定义的这个只是1个类型,和string,int32一样只是先定义1个类型,1个池子,你要用的话,就得来申请。来个完整的例子:

  1. message SearchRequest {
  2. string query = 1;
  3. int32 page_number = 2;
  4. int32 result_per_page = 3;
  5. enum corpus {
  6. UNIVERSAL = 0;
  7. web = 1;
  8. IMAGES = 2;
  9. LOCAL = 3;
  10. NEWS = 4;
  11. PRODUCTS = 5;
  12. VIDEO = 6;
  13. }
  14. corpus ken_leb = 4;
  15. }

我们定义了Corpus这个枚举类型,然后用Corpus ken_leb = 4; 来申明了这个类型的字段ken_leb。
我们看下,这样生成的go语言长啥样:

  1. $ protoc --go_out=. emeu.proto

我们截取片段来看下:

  1. type SearchRequestCorpus int32
  2. const (
  3. SearchRequest_UNIVERSAL SearchRequestCorpus = 0
  4. SearchRequest_web SearchRequestCorpus = 1
  5. SearchRequest_IMAGES SearchRequestCorpus = 2
  6. SearchRequest_LOCAL SearchRequestCorpus = 3
  7. SearchRequest_NEWS SearchRequestCorpus = 4
  8. SearchRequest_PRODUCTS SearchRequestCorpus = 5
  9. SearchRequest_VIDEO SearchRequestCorpus = 6
  10. )
  11. // Enum value maps for SearchRequestCorpus.
  12. var (
  13. SearchRequestCorpus_name = map[int32]string{
  14. 0: "UNIVERSAL",
  15. 1: "web",
  16. 2: "IMAGES",
  17. 3: "LOCAL",
  18. 4: "NEWS",
  19. 5: "PRODUCTS",
  20. 6: "VIDEO",
  21. }
  22. SearchRequestCorpus_value = map[string]int32{
  23. "UNIVERSAL": 0,
  24. "web": 1,
  25. "IMAGES": 2,
  26. "LOCAL": 3,
  27. "NEWS": 4,
  28. "PRODUCTS": 5,
  29. "VIDEO": 6,
  30. }
  31. )
  32. type SearchRequest struct {
  33. Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
  34. PageNumber int32 `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"`
  35. ResultPerPage int32 `protobuf:"varint,3,opt,name=result_per_page,json=resultPerPage,proto3" json:"result_per_page,omitempty"`
  36. KenLeb SearchRequestCorpus `protobuf:"varint,4,opt,name=ken_leb,json=kenLeb,proto3,enum=protoA.SearchRequestCorpus" json:"ken_leb,omitempty"`
  37. }

通过前面的学习,这一段基本可以看懂的非常明白了。首先,go定义了1个名为SearchRequestCorpusint32类型的自定义类型。然后分别申明了6个常量来承接这个枚举,还很贴心了搞了2个map,根据名字找数字值,和根据数字值来找名字。

值得注意的是转换后的名字的规则。

我们在message SearchRequest 中定义1个名字名为corpus的枚举类型。生成的go代码里,这个类型就叫做:SearchRequestCorpus 同样也帮我们把首字母给自动大写了。

其次,枚举里面具体的每一个成员,他们的名字,也会叫上SearchRequest_前缀,然后拼上自己的名字,但是自己的名字,首字母大小写确不会给转。因为go是用const常量来承接这个枚举的,就会原样输出:

  1. SearchRequest_web //web没转成首字母大小写
  2. SearchRequest_UNIVERSAL

我们再来看转换后的结构体中SearchRequest中的成员,也都按照规划,转成了首字母大写。

ptoro中,枚举是既可以放在message里面,又可以放在外面,比如:

  1. syntax = "proto3";
  2. package protoA;
  3. option go_package = ".;protoA";
  4. enum Day {
  5. MON = 0;
  6. TUE = 1;
  7. }
  8. message SearchRequest {
  9. string query = 1;
  10. enum corpus {
  11. UNIVERSAL = 0;
  12. web = 1;
  13. }
  14. corpus ken_leb = 4;
  15. }
  16. message Response {
  17. Day day = 1;
  18. }

区别在于:放在最外面,就是全局的,可以被多个message引用,放在单独1个message里面,就是私有的,只能被自己使用。

我们看下这样生成后的go代码长啥样:

  1. type Day int32
  2. const (
  3. Day_MON Day = 0
  4. Day_TUE Day = 1
  5. )
  6. // Enum value maps for Day.
  7. var (
  8. Day_name = map[int32]string{
  9. 0: "MON",
  10. 1: "TUE",
  11. }
  12. Day_value = map[string]int32{
  13. "MON": 0,
  14. "TUE": 1,
  15. }
  16. )
  17. type Response struct {
  18. Day Day `protobuf:"varint,1,opt,name=day,proto3,enum=protoA.Day" json:"day,omitempty"`
  19. }

我只截取了枚举放在外面的情况生成的代码,可以看出,生成的东西和刚才放在里面是一摸一的。唯一的区别就是命名,常量的前缀就变成了自己的名字:Day_

  1. Day_MON
  2. Day_TUE

7. 他消息类型(repeated)

在ptoro2有字段前面可以加一个约定。比如:required表示是这个字段必须有的,optional表示这个字段是可选的。然而,在ptoro3中,这2个都被去掉了。所以生成的字段都是可选可无的。只保留了repeated,表示这个字段是重复的,可以是0,或者多个,一般用来生成数组或者切片类型。我们直接看例子:

  1. #proto2
  2. message Result {
  3. required string url = 1; //必须包含
  4. optional string title = 2; //可有可无
  5. repeated string snippets = 3; //重复
  6. }
  1. #proto3
  2. message Result {
  3. repeated string snippets = 3; //重复
  4. }

既然去掉了,我们就不看了,我们就看repeated生成后的语言长啥样。我们生成go看一下:

  1. type Result struct {
  2. Snippets []string `protobuf:"bytes,3,rep,name=snippets,proto3" json:"snippets,omitempty"` //重复
  3. }

帮我们转成了1个string类型的切片。

8. import 导入另一个proto文件

在proto中,我们可以用import关键字导入另一个proto文件。这一点和其他语言的import用法是一样的:

  1. import public "new.proto";
  2. import "other.proto";
  3. import "google/protobuf/any.proto";

需要注意的事情有2个:

1,导入的其他proto协议文件,必须要使用才可以,不然编译会报错:

  1. (base) proto git:(master) protoc --go_out=. emeu.proto hello.proto
  2. emeu.proto:7:1: warning: Import hello.proto is unused.

2, 导入的proto文件,编译的时候,一定要全部带上:

  1. protoc --go_out=. emeu.proto hello.proto common.proto //全部带上
  2. protoc --go_out=. *proto //或者用*通配符

3、导入只能1层,不能嵌套导入(即:导入的文件A里面,不能再导入1个文件B),如果有这样需求的话,可以在嵌套导入的前面加个public。这样编译的时候会单独生成1个pb.go文件。

不加public的导入,其实都是把另一个proto的文件复制到我当前这个文件下,相当于合并成了1个文件,这样生成的ph.go文件只会是一个。

如果加了public,就会单独生成另一个pb.go文件,这样就可以通过夸文件调用即可。我们举个例子来说明:

  1. //emeu.proto文件,导入了hello.proto
  2. syntax = "proto3";
  3. package protoA;
  4. option go_package = ".;protoA";
  5. import "hello.proto";
  6. enum Day {
  7. MON = 0;
  8. TUE = 1;
  9. }
  1. //hello.proto
  2. syntax = "proto3";
  3. package protoA;
  4. option go_package = ".;protoA";
  5. enum WEEK {
  6. DOO = 0;
  7. POO = 1;
  8. }

这个时候,我们执行一下:会报错,说hello.proto导入了,但是没使用:

  1. protoc --go_out=. emeu.proto hello.proto
  2. emeu.proto:7:1: warning: Import hello.proto is unused.

我们使用一下:

  1. //emeu.proto文件,导入了hello.proto
  2. syntax = "proto3";
  3. package protoA;
  4. option go_package = ".;protoA";
  5. import "hello.proto";
  6. enum Day {
  7. MON = 0;
  8. TUE = 1;
  9. }
  10. message Response {
  11. Day day = 1;
  12. string jack = 2;
  13. protoA.WEEK teem = 3;
  14. }

这个时候,执行就不会报错了,注意使用方式:package名字+emeu名字 protoA.WEEK,而且就生成了一个pb.go文件。

我们这个继续创建一个common.proto,在hello里面导入:

  1. //common.proto
  2. syntax = "proto3";
  3. package protoA;
  4. option go_package = ".;protoA";
  5. enum Status {
  6. ok = 0;
  7. fail = 1;
  8. }

修改hello.proto,在里面导入common.proto

  1. syntax = "proto3";
  2. package protoA;
  3. option go_package = ".;protoA";
  4. import "common.proto";
  5. enum WEEK {
  6. DOO = 0;
  7. POO = 1;
  8. }

这个使用调用关系就是:

emeu ->  hello -> common

我们执行一下,记得把涉及的文件都带上:同时提示·common.proto没使用。

  1. $ protoc --go_out=. emeu.proto hello.proto common.proto
  2. hello.proto:7:1: warning: Import common.proto is unused.

那我们改下emeu,里面使用一下这个common.proto里的Status

  1. ...
  2. message Response {
  3. Day day = 1;
  4. string jack = 2;
  5. protoA.WEEK teem = 3;
  6. protoA.Status gbk = 4;
  7. }

然后再执行一下,提示:

  1. hello.proto:7:1: warning: Import common.proto is unused.
  2. emeu.proto:29:5: "protoA.Status" seems to be defined in "common.proto", which is not imported by "emeu.proto". To use it here, please add the necessary import.

意思就是虽然使用了,但是并没有被直接导入,这是不允许的。我们怎么办呢?我们可以这样改,导入前面加个public,表示是一个公共的类,不需要复制进来,需要单独编译成1个go文件

  1. //hello.proto
  2. syntax = "proto3";
  3. package protoA;
  4. option go_package = ".;protoA";
  5. import public "common.proto";
  6. enum WEEK {
  7. DOO = 0;
  8. POO = 1;
  9. }

我们再执行下,成功了,并且生成了3个pb.go文件:

  1. -rw-r--r-- 1 jim user00 3.5K Dec 11 12:16 common.pb.go //1
  2. -rw-r--r-- 1 jim user00 112B Dec 11 10:32 common.proto
  3. -rw-r--r-- 1 jim user00 6.5K Dec 11 12:16 emeu.pb.go //2
  4. -rw-r--r-- 1 jim user00 470B Dec 11 12:14 emeu.proto
  5. -rw-r--r-- 1 jim user00 3.6K Dec 11 12:16 hello.pb.go //3
  6. -rw-r--r-- 1 jim user00 421B Dec 11 12:14 hello.proto

我们打开看一下,可以看到是通过导入其他go文件的方式进行调用的,而不是全部复制到1个go文件里的。

总结一下:

  1. 1. 导入不加public 是复制到1proto文件里,只会生成1go文件
  2. 2. 导入加了public,就是单独另生成1go文件。
  3. 3. 不支持直接嵌套导入(除非加public

9. 嵌套类型(嵌套message)

message是支持嵌套的,这个和go里面的结构体strut类似,我们直接看一下例子:

  1. message SearchResponse {
  2. message Result {
  3. string url = 1;
  4. string title = 2;
  5. repeated string snippets = 3;
  6. }
  7. repeated Result results = 1;
  8. }

语法含义,应该是很清楚的,我们直接看下编译生成的go代码是咋样的:

首先是最外面的SearchResponse结构体的转换,repeated表示是1个切片数组:

  1. type SearchResponse struct {
  2. state protoimpl.MessageState
  3. sizeCache protoimpl.SizeCache
  4. unknownFields protoimpl.UnknownFields
  5. Results []*SearchResponse_Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
  6. }

再看下子元素:

  1. type SearchResponse_Result struct {
  2. state protoimpl.MessageState
  3. sizeCache protoimpl.SizeCache
  4. unknownFields protoimpl.UnknownFields
  5. Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
  6. Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
  7. Snippets []string `protobuf:"bytes,3,rep,name=snippets,proto3" json:"snippets,omitempty"`
  8. }

基本就是按照message里设定的来生成的,这个基本没啥疑问。我们继续看。

如果在proto中如果想引用其他的message里面的嵌套message,怎么弄呢?

同一个proto文件:

  1. message SearchResponse {
  2. message Result {
  3. string url = 1;
  4. string title = 2;
  5. repeated string snippets = 3;
  6. }
  7. repeated Result results = 1;
  8. }
  9. message SearchInfo {
  10. repeated SearchResponse.Result results = 2;
  11. }

不同的proto文件:通风package名.message1.message2

  1. import "common.proto";
  2. message Response {
  3. string jack = 2; //重复
  4. protoA.SearchResponse.Result cnb = 5; //导入其他proto文件里的嵌套message
  5. }

10. 任意类型(Any)

类似于go中的interface{}空接口可以表示任意类型一样,proto里面的变量的类型也可以先设定为任意的,不过得导入Google的一个文件:

  1. import "google/protobuf/any.proto";
  2. message ErrorStatus {
  3. string message = 1;
  4. repeated google.protobuf.Any details = 2;
  5. }

这样,details 就是一个任意的类型了,这个不错。我们看下生成的go长啥样:

  1. import any "github.com/golang/protobuf/ptypes/any"
  2. type ErrorStatus struct {
  3. state protoimpl.MessageState
  4. sizeCache protoimpl.SizeCache
  5. unknownFields protoimpl.UnknownFields
  6. Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
  7. Details []*any.Any `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"`
  8. }

它引入了一个any.Any指针类型。具体这个怎么使用的呢?先不考虑,我们后续再讲。只需要知道,用这个可以表示任意类型即可。

11. 其中之一(Oneof)

直接先看个例子:

  1. message SampleMessage {
  2. oneof test_oneof {
  3. string name = 4;
  4. SubMessage sub_message = 9;
  5. }
  6. }

遇到了一个新的关键字oneof,咋一眼,字面意思是其中之一,如果你的message中有很多可选字段, 并且同时至多一个字段会被设置,那么你就可以用。而且使用oneof特性能够节省内存。
Oneof字段就像可选字段,除了它们会共享内存,至多一个字段会被设置。设置其中一个字段会清除其它字段。

先知道这么多就可以了,具体用的使用我们再看使用方法。除了maprepeated字段类型不可用外,其他的类型都可以。

12. Map

我们再message中,还可以增加另一种类型map,这种类型基本上和go语法中的功能是一样的,就是一个key-value对。需要注意的是:key可以是任意的proto变量('int32', 'int64', 'uint32', 'uint64', 'sint32', 'sint64', 'fixed32', 'fixed64', 'sfixed32', 'sfixed64', 'bool', 'string')中的类型,除去floatbytes。 value则可以是任何类型,也包括自定义的message和emeu类型。

  1. message SampleMessage {
  2. map<string, string> name = 1;
  3. }

编译成go看看:

  1. type SampleMessage struct {
  2. Name map[string]string `protobuf:"bytes,1,rep,name=name,proto3" json:"name,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
  3. }

可以看出,确实是按照map的类型设置的。比较简单。需要注意的是key只能是普调类型()

我们改一下类型看看:

  1. message SampleMessage {
  2. map<string, string> name = 1; //value 是 普调的string
  3. map<int32, SearchResponse.Result> age = 2; //value 嵌套的栓层message
  4. map<bool, Status> sex = 3; //value 是enum枚举
  5. }

看看生成的go语言:

  1. type SampleMessage struct {
  2. Name map[string]string `protobuf:"bytes,1,rep,name=name,proto3" json:"name,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` //value 是 普调的string
  3. Age map[int32]*SearchResponse_Result `protobuf:"bytes,2,rep,name=age,proto3" json:"age,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` //value 嵌套的栓层message
  4. Sex map[bool]Status `protobuf:"bytes,3,rep,name=sex,proto3" json:"sex,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3,enum=protoA.Status"` //value 是enum枚举
  5. }

13. 定义Services

这是proto中最后的一个特性,也是最重要的。它用来定义一个对外的服务。gRpc的服务就是这样定义的。它的写法为:

  1. service SearchService {
  2. rpc Search(SearchRequest) returns (SearchResponse) {}
  3. }

格式就是上面这样子,值得注意的是,rpc后面的函数,括号里,它的参数和返回值都必须是一个message类型。不能是其他类型,而且只能是1个参数,不能是多个

  1. rpc RpcName1(message1) returns (message2)
  2. rpc RpcName2(message3) returns (message4)
  3. rpc RpcName3(message5) returns (message6)

我们搞个实际的例子,结合gRpc来看看,这个service要怎么实现。

  1. syntax = "proto3";
  2. package proto;
  3. option go_package = ".;proto";
  4. message Id {
  5. int32 id=1;
  6. }
  7. message Name {
  8. string name=1;
  9. }
  10. message Age {
  11. int32 age=1;
  12. }
  13. // 用户变量
  14. message User {
  15. int32 id=1;
  16. string name=2;
  17. int32 age=3;
  18. }
  19. // 用户参数
  20. message UserParams{
  21. Name name=1;
  22. Age age=2;
  23. }
  24. // 声明那些方法可以使用rpc
  25. service ServiceSearch{
  26. rpc SaveUser(UserParams) returns (Id){}
  27. rpc UserInfo(Id) returns (User){}
  28. }
  29. //执行 : protoc --go_out=plugins=grpc:. *.proto

代码如上面所示,我们定义了1个ServiceSearch的服务,里面有2个rpc方法:SaveUser它的接受参数是UserParams,返回值是Id。都是2个message。同理,UserInfo方法也是一样。

通过这2个rpc的定义,我们其实更看出个七七八八,定义的2个方法,和普通的函数非常的类似。接下来,我们转换成go语言,看看具体长啥样:

  1. //注意:需要加上plugins=grpc: 表示,用grpc生成服务。
  2. protoc --go_out=plugins=grpc:. server.proto

会生成1个server.pb.go的文件,我们简单看下这个文件的关键地方。

先是把5个message分别转换成了5个对应的struct,这个没啥问题:

  1. type Id struct {
  2. Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
  3. }
  4. type Name struct {
  5. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  6. }
  7. type Age struct {
  8. Age int32 `protobuf:"varint,1,opt,name=age,proto3" json:"age,omitempty"`
  9. }
  10. type User struct {
  11. Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
  12. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
  13. Age int32 `protobuf:"varint,3,opt,name=age,proto3" json:"age,omitempty"`
  14. }
  15. type UserParams struct {
  16. Name *Name `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  17. Age *Age `protobuf:"bytes,2,opt,name=age,proto3" json:"age,omitempty"`
  18. }

然后就是rpc服务的转换,先是生成client的一个接口,里面包含我们定义的2个方法。

  1. type ServiceSearchClient interface {
  2. SaveUser(ctx context.Context, in *UserParams, opts ...grpc.CallOption) (*Id, error)
  3. UserInfo(ctx context.Context, in *Id, opts ...grpc.CallOption) (*User, error)
  4. }

再分别实现了这个接口,实现了这2个方法

  1. type serviceSearchClient struct {
  2. cc grpc.ClientConnInterface
  3. }
  4. func (c *serviceSearchClient) SaveUser(ctx context.Context, in *UserParams, opts ...grpc.CallOption) (*Id, error) {
  5. out := new(Id)
  6. err := c.cc.Invoke(ctx, "/proto.ServiceSearch/SaveUser", in, out, opts...)
  7. if err != nil {
  8. return nil, err
  9. }
  10. return out, nil
  11. }
  12. func (c *serviceSearchClient) UserInfo(ctx context.Context, in *Id, opts ...grpc.CallOption) (*User, error) {
  13. out := new(User)
  14. err := c.cc.Invoke(ctx, "/proto.ServiceSearch/UserInfo", in, out, opts...)
  15. if err != nil {
  16. return nil, err
  17. }
  18. return out, nil
  19. }

然后,生成了对应的Server服务端方法:

  1. type ServiceSearchServer interface {
  2. SaveUser(context.Context, *UserParams) (*Id, error)
  3. UserInfo(context.Context, *Id) (*User, error)
  4. }
  5. type UnimplementedServiceSearchServer struct {
  6. }
  7. func (*UnimplementedServiceSearchServer) SaveUser(context.Context, *UserParams) (*Id, error) {
  8. return nil, status.Errorf(codes.Unimplemented, "method SaveUser not implemented")
  9. }
  10. func (*UnimplementedServiceSearchServer) UserInfo(context.Context, *Id) (*User, error) {
  11. return nil, status.Errorf(codes.Unimplemented, "method UserInfo not implemented")
  12. }

ok。关于proto的语法就到此结束了。下面接下来会去学习,grpc的东西,用go和php来服务对接。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注