王中阳Go

V1

2022/11/08阅读:28主题:橙心

【Go微服务】一文带你玩转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查看自己的芯片类型点击左上角的苹果图标,再点击关于本机,就可以查看了。

image.png
image.png

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

image.png
image.png

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

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

Mac安装小技巧

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

image.png
image.png

1.3 编译go语言的工具包

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

命令是这样的:

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

或者

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

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的文件

image.png
image.png

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

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

10分钟进阶

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

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

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

image.png
image.png

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 Type Go Type 使用技巧
double float64 没特殊技巧,记住float对应go的float32,double对应go的float64就可以了
float float32 没特殊技巧,记住float对应go的float32,double对应go的float64就可以了
int32 int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代
uint32 uint32 使用变长编码
uint64 uint64 使用变长编码
sint32 int32 使用变长编码,这些编码在负值时比int32高效的多
sint64 int64 使用变长编码,有符号的整型值。编码时比通常的int64高效。
fixed32 uint32 总是4个字节,如果数值都比228大的话,这个类型会比uint32高效。
fixed64 uint64 总是8个字节,如果数值都比256大的话,这个类型会比uint64高效。
sfixed32 int32 总是4个字节
sfixed64 int64 总是8个字节
bool bool 严格对应,玩不出其他花样来
string string 一个字符串必须是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编译器无法编译通过,将会输出警告信息。

image.png
image.png

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消息,执行效果如下:

image.png
image.png

下载再带小伙伴们了解一下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枚举类型
}

运行效果如下:

image.png
image.png

在实际开发中,我们需要定义很多的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文件:

image.png
image.png

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<stringint32> score = 3;  //学科 分数的map
}

运行效果如下: image.png

再强调一下

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

image.png
image.png

至此我们已经掌握了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<stringint32> 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)
}

运行结果如下:

image.png
image.png

本文总结

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

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

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

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

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

关于专栏

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

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

image.png
image.png

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

一起学习,升级打怪

我们搞了一个对学Go真正有帮助的群,欢迎加入:

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

微信号:wangzhongyang1993

参考资料

[1]

https://juejin.cn/column/7140137480749547528: https://juejin.cn/column/7140137480749547528

分类:

后端

标签:

Golang

作者介绍

王中阳Go
V1

专注Go语言开发