草庐IT

一文带你玩转ProtoBuf

王中阳Go 2023-04-09 原文

前言

在网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 或 XML,在微服务架构中通常使用另外一个数据交换的协议的工具ProtoBuf。

ProtoBuf也是我们做微服务开发,进行Go进阶实战中,必知必会的知道点。

今天就开始第一章内容:《一文带你玩转ProtoBuf》

5分钟入门

1.1 简介

你可能不知道ProtoBuf,但一定知道json或者xml,从一定意义上来说他们的作用是一样的。

ProtoBuf全称:protocol buffers,直译过来是:“协议缓冲区”,是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。

和json\xml最大的区别是:json\xml都是基于文本格式,ProtoBuf是二进制格式。

ProtoBuf相比于json\XML,更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

我们只需要定义一次数据结构,就可以使用ProtoBuf生成源代码,轻松搞定在各种数据流和各种语言中写入、读取结构化数据。

1.2 安装

建议大家使用主流版本v3,这是官网下载地址:https://github.com/protocolbuffers/ProtoBuf/releases

注意,不同的电脑系统安装包是不一样的:

  • Windows 64位 点这里下载

  • Windows 32位 点这里下载

  • Mac Intel 64位 点这里下载

  • Mac ARM 64位 点这里下载

  • Linux 64位 点这里下载

(公众号无法跳转到外链,点击文末的阅读原文可以跳转到下载地址。)

小技巧:Mac查看自己的芯片类型点击左上角的苹果图标,再点击关于本机,就可以查看了。

比如,我的处理器芯片是intel的,下载安装包之后是这样的:

bin目录下的protoc是ProtoBuf的工具集,下文会重点介绍它的使用。

注意:我们需要将下载得到的可执行文件protoc所在的 bin 目录加到我们电脑的环境变量中。

Mac安装小技巧

如果你的Mac安装了brew,安装ProtoBuf就更简单了,我们使用brew install ProtoBuf就可以了

1.3 编译go语言的工具包

这个protoc可以将proto文件编译为任何语言的文件,想要编译为go语言的,还需要下载另外一个可执行文件

命令是这样的:

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

1.4 编写proto代码

下面就编写一个非常简单,但是五脏齐全的proto代码,我们再根据这段代码生成pb.go文件。

syntax = "proto3";

package hello;

option go_package = "./;hello";

message Say{
  int64           id    = 1;
  string          hello = 2;
  repeated string word  = 3;
}

1.5 生成go代码

生成go代码,非常简单,使用下面的命令就可以了。

切换到.proto文件所在目录

cd proto/demo/

指定proto源文件,自动生成代码。

protoc --go_out=. hello.proto

执行上面的命令后,我们在项目中就自动生成了一个.pb.go的文件

入门ProtoBuf就是这么的简单:通过这几步我们就完成了ProtoBuf的下载、安装、编写了一个proto文件,并生成了能用Go语言读写ProtoBuf的源代码。

我们再深入了解一下probuf的用法:

10分钟进阶

下面再带大家深入了解一下ProtoBuf的知识点,避免在开发中踩坑。

小技巧:写proto和写go最大的区别是需要在结尾添加分号的;,在开发过程中给自己提个醒:如果是写proto需要加分号,如果是写go不需要加分号。

以我们上面的proto入门代码举例:

1.1 关键字

  • syntax:是必须写的,而且要定义在第一行;目前proto3是主流,不写默认使用proto2

  • package:定义我们proto文件的包名

  • option go_package:定义生成的pb.go的包名,我们通常在proto文件中定义。如果不在proto文件中定义,也可以在使用protoc生成代码时指定pb.go文件的包名

  • message:非常重要,用于定义消息结构体,不用着急,下文会重点讲解

细心的小伙伴一定注意到了 message 消息体中有一个 “repeated” 关键字,这在我们写Go的时候是没有的。

这是干什么用的呢?下面来详细解答一下:

1.2 数组类型

关于数组类型,和Java、Go、PHP等语言中,定义数据类型不一样。

在ProtoBuf消息中定义数组类型,是通过在字段前面增加repeated关键词实现,标记当前字段是一个数组。

只要使用repeated标记类型定义,就表示数组类型。

我们来举两个例子:

1.整数数组:

下面定义的arrays表示int32类型的数组

message Msg {
  repeated int32 arrays = 1;
}

2.字符串数组

下面定义的names表示字符串数组

message Msg {
  repeated string names = 1;
}

repeated搞懂了,message又是干嘛用的呢?

1.3 消息

消息(message),在ProtoBuf中指的就是我们要定义的数据结构。类似于Go中定义结构体。

message关键词用法也非常简单:

1. 语法

syntax = "proto3";

message 消息名 {
    消息体
}

例子:

syntax = "proto3";
 
message Request {
  string query = 1;
  int32  page = 2;
  int32  limit = 3;
}

定义了一个Request消息,这个消息有3个字段,query是字符串类型,page和limit是int32类型。

1.4 字段类型

ProtoBuf支持多种数据类型,例如:string、int32、double、float等等,我整理了一份ProtoBuf和go语言的数据类型映射表

.proto TypeGo Type使用技巧
doublefloat64没特殊技巧,记住float对应go的float32,double对应go的float64就可以了
floatfloat32没特殊技巧,记住float对应go的float32,double对应go的float64就可以了
int32int32使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代
uint32uint32使用变长编码
uint64uint64使用变长编码
sint32int32使用变长编码,这些编码在负值时比int32高效的多
sint64int64使用变长编码,有符号的整型值。编码时比通常的int64高效。
fixed32uint32总是4个字节,如果数值都比228大的话,这个类型会比uint32高效。
fixed64uint64总是8个字节,如果数值都比256大的话,这个类型会比uint64高效。
sfixed32int32总是4个字节
sfixed64int64总是8个字节
boolbool严格对应,玩不出其他花样来
stringstring一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。
bytes[]byte可以包含任意顺序的字节数组

1.5 分配标识号

细心的小伙伴可能又有疑问了,上面消息体中的 string query = 1; 这个1是什么呢?

这些数字是“分配表示号”:在消息定义中,每个字段后面都有一个唯一的数字,这个就是标识号。

这些标识号的作用是:用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。

注意:分配标识号在每个消息内唯一,不同的消息体是可以拥有相同的标识号的。

小技巧:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。

1.5.1 保留标识号(Reserved)

小技巧:要为将来有可能添加的、频繁出现的字段预留一些标识号。

我们想保留一些标识号,留给以后用,可以使用下面语法:

message Test {
  reserved 2, 5, 7 to 10; // 保留2,5,7到10这些标识号
}

如果使用了这些保留的标识号,protocol buffer编译器无法编译通过,将会输出警告信息。

1.6 将消息编译成各种语言版本的类库

编译器命令格式:

protoc [OPTION] PROTO_FILES

OPTION是命令的选项, PROTO_FILES是我们要编译的proto消息定义文件,支持多个。

常用的OPTION选项:

  --go_out=OUT_DIR            指定代码生成目录,生成 Go 代码
  --cpp_out=OUT_DIR           指定代码生成目录,生成 C++ 代码
  --csharp_out=OUT_DIR        指定代码生成目录,生成 C# 代码
  --java_out=OUT_DIR          指定代码生成目录,生成 java 代码
  --js_out=OUT_DIR            指定代码生成目录,生成 javascript 代码
  --objc_out=OUT_DIR          指定代码生成目录,生成 Objective C 代码
  --php_out=OUT_DIR           指定代码生成目录,生成 php 代码
  --python_out=OUT_DIR        指定代码生成目录,生成 python 代码
  --ruby_out=OUT_DIR          指定代码生成目录,生成 ruby 代码

因为开篇我们就用Go举了例子,下面再用Java举个例子吧:

protoc --java_out=. hello.proto

在当前目录导出java版本的代码,编译hello.proto消息,执行效果如下:

下载再带小伙伴们了解一下ProtoBuf的进阶知识点吧:枚举类型、消息嵌套和Map类型。

1.7 枚举类型

写Java的同学枚举一定用的很溜,但是写Go的同学可能有点懵了,Go是不直接支持枚举的,并没有Enum关键字。

关注我,后续会详解Go枚举相关的知识点,在这篇文章中不做重点介绍。

使用枚举的场景是这样的:

当定义一个消息类型的时候,可能想为一个字段指定“预定义值”中的其中一个值,这时候我们就可以通过枚举实现,比如这种:

syntax = "proto3";//指定版本信息,非注释的第一行

enum SexType //枚举消息类型,使用enum关键词定义,一个性别类型的枚举类型
{
    UNKONW = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
    MALE = 1;  //1男
    FEMALE = 2; //2女  0未知
}

// 定义一个用户消息
message UserInfo
{
    string name = 1; // 姓名字段
    SexType sex = 2; // 性别字段,使用SexType枚举类型
}

运行效果如下:

在实际开发中,我们需要定义很多的proto,我们如何做到消息的复用呢?

答案就是:“消息嵌套”

1.8 消息嵌套

我们在开发Java和PHP时,经常嵌套使用类,也可以使用其他类作为自己的成员属性类型;在开发Go时经常嵌套使用结构体。

在ProtoBuf中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。

我们来看下面3个经典示例:

1.8.1 引用其他消息类型的用法

// 定义Article消息
message Article {
  string url = 1;
  string title = 2;
  repeated string tags = 3; // 字符串数组类型
}

// 定义ListArticle消息
message ListArticle {
  // 引用上面定义的Article消息类型,作为results字段的类型
  repeated Article articles = 1; // repeated关键词标记,说明articles字段是一个数组
}

1.8.2 消息嵌套

类似类嵌套一样,消息也可以嵌套,比如这样:

message ListArticle {
  // 嵌套消息定义
  message Article {
    string url = 1;
    string title = 2;
    repeated string tags = 3;
  }
  // 引用嵌套的消息定义
  repeated Article articles = 1;
}

1.8.3 import导入其他proto文件定义的消息

我们在实际开发中,通常要定义很多消息,如果都写在一个proto文件,是不方便维护的。

小技巧:将消息定义写在不同的proto文件中,在需要的时候可以通过import导入其他proto文件定义的消息。

例子:

创建文件: article.proto

syntax = "proto3";

package nesting;

option go_package = "./;article";

message Article {
  string          url   = 1;
  string          title = 2;
  repeated string tags  = 3; // 字符串数组类型
}

创建文件: list_article.proto

syntax = "proto3";
// 导入Article消息定义
import "article.proto";

package nesting;

option go_package = "./;article";

// 定义ListArticle消息
message ListArticle {
  // 使用导入的Result消息
  repeated Article articles = 1;
}

执行效果如下,我们顺利生成了.pb.go文件:

1.9 map类型

我们在Go语言开发中,最常用的就是切片类型和map类型了。

切片类型在ProtoBuf中对应的就是repeated类型,前面我们已经介绍过了。

再重点介绍一下map类型,ProtoBuf也是支持map类型的:

1.9.1 map语法

map<key_type, value_type> map_field = N;

语法非常简单和通用,但是有几个问题需要我们注意:

  1. key_type可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。

  2. 注意:枚举不是有效的key_type

  3. value_type 可以是除另一个映射之外的任何类型。

  4. Map 字段不能使用repeated关键字修饰。

1.9.2 map的例子

我们举个典型的例子:学生的学科和分数就适合用map定义:

syntax = "proto3";

package map;

option go_package = "./;score";

message Student{
  int64              id    = 1; //id
  string             name  = 2; //学生姓名
  map<string, int32> score = 3;  //学科 分数的map
}

运行效果如下: 

再强调一下

注意:Map 字段是不能使用repeated关键字修饰。

至此我们已经掌握了ProtoBuf的所有知识点,是不是非常简单清晰呢?

下面我们在Go项目中实战应用一下ProtoBuf,从ProtoBuf中读取数据,并且转换为我们常用的结构体

5分钟实战

1. 首先我们定义proto文件

我创建了一个demo目录,创建了名为study_info.proto的文件

syntax = "proto3";

package demo;

option go_package = "./;study";

message StudyInfo {
  int64              id       = 1; //id
  string             name     = 2; //学习的科目名称
  int32              duration = 3; //学习的时长 单位秒
  map<string, int32> score    = 4; //学习的分数
}

2. 生成代码

使用命令生成pb.go文件:

protoc --go_out=. study_info.proto

3.编写go文件

编写go文件,读取ProtoBuf中定义的字段,进行赋值,取值,转成结构体等操作:

proto编码和解码的操作和json是非常像的,都使用“Marshal”和“Unmarshal”关键字。

package main

import (
   "fmt"
   "google.golang.org/ProtoBuf/proto"
   study "juejin/ProtoBuf/proto/demo"
)

func main() {
   // 初始化proto中的消息
   studyInfo := &study.StudyInfo{}

   //常规赋值
   studyInfo.Id = 1
   studyInfo.Name = "学习ProtoBuf"
   studyInfo.Duration = 180

   //在go中声明实例化map赋值给ProtoBuf消息中定义的map
   score := make(map[string]int32)
   score["实战"] = 100
   studyInfo.Score = score

   //用字符串的方式:打印ProtoBuf消息
   fmt.Printf("字符串输出结果:%v\n", studyInfo.String())

   //转成二进制文件
   marshal, err := proto.Marshal(studyInfo)
   if err != nil {
      return
   }
   fmt.Printf("Marshal转成二进制结果:%v\n", marshal)

   //将二进制文件转成结构体
   newStudyInfo := study.StudyInfo{}
   err = proto.Unmarshal(marshal, &newStudyInfo)
   if err != nil {
      return
   }
   fmt.Printf("二进制转成结构体的结果:%v\n", &newStudyInfo)
}

运行结果如下:

本文总结

ProtoBuf作为开发微服务必选的数据交换协议,基于二进制传输,比json/xml更小,速度更快,使用也非常的简单。

通过这篇文章,我们不仅学会了ProtoBuf的入门操作,还使用Go语言基于ProtoBuf编码解码了数据,进行了实战。

进阶部分带大家了解了ProtoBuf如何定义消息、ProtoBuf和Go数据类型的映射、枚举类型如何使用、通过消息嵌套复用代码、使用map类型时需要注意的问题和小技巧。

微服务架构成为企业项目的必然选择已是趋势,如果你只会开发单体项目,请关注我,带你一起玩转微服务。

天下难事,必作于易。想都是问题,做才有答案。站着不动,永远是观众。

关于专栏

近期会更新一系列Go实战进阶的文章,欢迎大家关注我的签约专栏 :Go语言进阶实战。

这是近期会更新文章的知识脉络图,感兴趣的小伙伴可以关注一波,欢迎日常催更。

 

已完成

《一文玩转ProtoBuf》

《开发gRPC总共分三步》

《Go WEB进阶实战:基于GoFrame搭建的电商前后台API系统》

小伙伴们还想看哪些内容,欢迎在评论区留言。

有关一文带你玩转ProtoBuf的更多相关文章

  1. 一文解决关于VLAN所有的疑惑 - 2

    一文解决关于VLAN所有的疑惑VLAN基本概念为什么需要VLAN?怎么在交换机上划分VLAN,VLAN的工作原理有了子网,已经隔离了广播,还需要VLAN干啥?只进行子网划分,不进行VLAN划分VLAN划分与子网划分附加VLAN信息的方法VLAN划分交换机的端口类型(Access和Trunk)一、访问链接二、汇聚链接汇聚链接VLAN间通信为什么要进行VLAN间通信?路由器实现VLAN间通信路由器和交换机的连接方式通信细节三层交换机实现VLAN间通信加速VLAN间通信三层交换机与路由器三层交换机路由器路由器和交换机配合构建LAN的实例使用VLAN设计局域网的特点VLAN增加网络的灵活性不使用VLA

  2. 一文让你彻底掌握操作符(超详细教程) - 2

    ✅作者简介:大家好,我是小杨📃个人主页:「小杨」的csdn博客🔥系列专栏:小杨带你玩转C语言【初阶】🐳希望大家多多支持🥰一起进步呀!大家好呀!我是小杨。小杨花几天的时间将C语言中的操作符这部分知识做了一个大总结,在方便自己复习的同时也能够帮助到大家。通篇字数在一万字左右,可以算作是非常详细了,一文就可以带领大家彻底掌握操作符这部分内容,文章很长建议先收藏再看,防止下次想看就找不到啦。文章目录✍1,算术操作符✍2,移位操作符    🔍2.1,左移操作符    🔍2.2,右移操作符       ✨2.2.1,算术移位       ✨2.2.2,逻辑移位✍3,位操作符    🔍3.1,按位与&   

  3. c# - 如何使用 protobuf-net 处理 .proto 文件 - 2

    我已经开始使用protobuf-net库在我维护的一些程序之间进行通信。我还能够将消息从C#解码为Ruby。我的ruby​​ProtoBuflib使用.proto文件生成ruby​​代码。为了必须在尽可能少的地方进行更改,我想让protobuf-net使用相同的.proto文件。查看protobuf-net文件夹,有一个名为ProtoBufGenerator的Dll和protobufexe,但我找不到任何关于我是否可以通过这种方式使protobuf-net工作的说明。这可能吗? 最佳答案 查看MarcGravell的博客,thisp

  4. 一文掌握软件项目成本预算、估算的方法和成本控制的秘籍 - 2

    每个企业都希望在完成项目后获得盈利,但不少企业到了年终后才发现项目做了不少,公司却并没能达到预期,甚至还出现了亏损。那么钱究竟去了哪里?很多公司都搞不清楚原因,出现糊涂账较多的状况,这将会造成严重的后果,尤其在疫情影响下,大环境很恶劣,如果是大公司的事业部门出现亏损,就可能会导致事业部门解散;如果是小公司出现亏损,就很容易导致公司倒闭;怎样做才能确保我们所完成的项目都能获利?从财务角度看,要确保盈利必须做到合理估算成本,只有这样才能在对外签订合约时做出合理报价,在对内在开始项目前做出充分评估投入代价,同时在实施过程中还要控制成本得当,最后项目结束时才会有可能获得盈利。那么我们怎样才能准确的判断

  5. 大家沉迷短视频无法自拔?Python爬虫进阶,带你玩转短视频 - 2

    大家好,我是辣条。现在短视频可谓是一骑绝尘,吃饭的时候、休息的时候、躺在床上都在刷短视频,今天给大家带来python爬虫进阶:美拍视频地址加密解析。短视频js逆向解析抓取目标工具使用重点学习内容项目思路解析抓取目标目标网址:美拍视频工具使用开发环境:win10、python3.7开发工具:pycharm、Chrome工具包:requests、xpath、base64重点学习内容爬虫采集数据的解析过程js代码调试技巧js逆向解析代码Python代码的转换项目思路解析进入到网站的首页挑选你感兴趣的分类根据首页地址获取到进入详情页面的超链接的跳转地址找到对应加密的视频播放地址数据这个数据是静态的网页

  6. 一文详解COINDAO是什么? - 2

    COINDAO旨在重建社区信任和安全。基于皖北基因的强烈共识,COINDAO自发产生了一个共创、共建、共治、共享的协作组织。它专注于DAO投资管理协议,为新的优质项目创造增长技术和资金。COINDAO的使命就是为真正的优质项目打造一个去中心化、公开透明的平台,让各个赛道上的优质项目能够以更低的成本快速募集资金并向公众开放.打破头部垄断。让真正的爱好者直接获得早期参与优质项目的资格,不再遥不可及,构建生态应用的可信体系,打造人人参与共建、人人共享的去中心化DAO好处。生态系统,我们称之为COINDAO生态系统。COINDAO国内各大财经网站宣发如下:COINDAO国外各大财经网站宣发: COIN

  7. 一文吃透前端低代码的 “神仙生活” - 2

    今天来说说前端低代码有多幸福?低代码是啥?顾名思义少写代码……这种情况下带来的幸福有:代码写得少,bug也就越少(所谓“少做少错”),因此开发环节的两大支柱性工作“赶需求”和“修bug”就都少了;要测的代码少了,那么测试用例也可以少写了。所以,总结低代码带来的幸福感有这三大点:开发效率提高开发成本减少维护性更高针对上述三点,我们展开说说。01、开发效率提高对于低代码的理解,个人认为可以通过配置化的低成本交互方式(主流是拖拽)加上少量的胶水代码,去满足一类应用的需求。这就说明,基于低代码,开发人员无需代码或说只需少量代码就可以开发出各类应用管理系统,如:OA协同办公、KM知识管理、CRM客户关系

  8. 【C语言进阶】还说不会?一文带你全面掌握计算机预处理操作 - 2

    目录🍊前言🍊:🍈一、宏与函数🍈:        1.宏与函数对比:    2.宏与函数的命名约定:🍓二、预处理操作符🍓:    1.预处理操作符"#":    2.预处理操作符"##":🥝三、条件编译🥝:    1.简述条件编译指令:    2.常见条件编译指令:🍒总结🍒:🛰️博客主页:✈️銮同学的干货分享基地🛰️欢迎关注:👍点赞🙌收藏✍️留言🛰️系列专栏:💐【进阶】C语言学习            🧧  C语言学习🛰️代码仓库:🎉VS2022_C语言仓库    家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!    

  9. 一文带你通俗理解23种软件设计模式(推荐收藏,适合小白学习,附带C++例程完整源码) - 2

    作者:翟天保Steven版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处一、设计模式是什么?    设计模式是为了解决在软件开发过程中遇到的某些问题而形成的思想。同一场景有多种设计模式可以应用,不同的模式有各自的优缺点,开发者可以基于自身需求选择合适的设计模式,去解决相应的工程难题。    良好的软件设计和架构,可以让代码具备良好的可读性、可维护性、可扩展性、可复用性,让整个系统具备较强的鲁棒性和性能,减少屎山代码出现的概率。    想要熟练运用设计模式,提高自己的编程能力和架构能力,只有在自己工作中,结合自身工作内容,多思考多实践。本文只能通过举一些通俗的例子,来

  10. 一文读懂Elephant Swap,为何为ePLATO带来如此高的溢价? - 2

    自今年4月以来,在经历了UST脱锚,所引发的头部投资机构、独角兽CeFi平台的相继“破产”,加密行业迎来了前所未有的发展危机,一方面在于市场信心不足甚至恐慌,导致的资金加速出逃,另一方面市场整体增速缓慢导致各个板块收益疲软,导致投资者们难以获得可观的收益回报,市场进一步陷入新一轮的恶性循环。当然,即便是市场整体处于下行周期,但加密市场仍旧存在诸多的获利机会,新型LaaS协议ElephantSwap正在通过其独特的LaaS方案,来为DeFi市场恢复流动性注入信心,同时为投资者带来十分可观的套利空间。目前,PlatoFarm是首个使用ElephantSwap流动性服务的项目,投资可以通过将手中的P

随机推荐