写点什么

一文带你玩转 ProtoBuf

作者:王中阳Go
  • 2022-10-13
    北京
  • 本文字数:5538 字

    阅读完需:约 1 分钟

一文带你玩转ProtoBuf

前言

在网络通信和通用数据交换等应用场景中经常使用的技术是 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


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



小技巧: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 语言的数据类型映射表


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 真正有帮助的群,欢迎加入:

公众号:程序员升级打怪之旅

微信号:wangzhongyang1993

发布于: 2022-10-13阅读数: 81
用户头像

王中阳Go

关注

公众号:程序员升级打怪之旅 2022-10-09 加入

微信:wangzhongyang1993

评论 (1 条评论)

发布
用户头像
用Go开发微服务的大佬,欢迎一起探讨。
2022-10-13 15:13 · 北京
回复
没有更多了
一文带你玩转ProtoBuf_Go_王中阳Go_InfoQ写作社区