Go微服务(二)——Protobuf详细入门 中

本文涉及的产品
云原生网关 MSE Higress,422元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: Go微服务(二)——Protobuf详细入门 中

3. Protobuf基本用法

首先看下下面这个proto文件,我们后面的proto基本用法都是基于这个proto进行讲解

syntax = "proto3";
package pkgName;
option go_package = "./";
message mmData {
  optional int32 num = 1;
  optional int32 def_num = 2 [default=10];
  required string str = 3;
  repeated string rep_str = 4;
}

0. 使用protoc 生成go文件

(可看完·3. Protobuf的基本用法·,在来看这部分)

---my_project
   |---06-protocol_buffers
       |---pbrpc
            |---service
                   |---service.proto

pbrpc/service/service.proto:

syntax = "proto3";
package hello;
// go module = MicroServiceStudy01
option go_package = "MicroServiceStudy01/06-protocol_buffers/pbrpc/service";
message Request{
  string value = 1;
}

使用protoc:

cd 06-protocol_buffers/pbrpc/service
protoc -I . --go_out=. hello.proto

如果这样执行的话,他的结果是在你go_out目录(这里是当前目录)存放,并且按照你定义的go_package的名称,在你go_out目录下创建一个目录结构:

如果你不想让他帮你生成一个go_package的目录结构,那么就需要指定一个前缀:

protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/06-protocol_buffers/pbrpc/service" hello.proto

这样就没有再根据go_pacakage生成目录结构,而是直接存放在了go_out目录:

我不理解,如果目的是存放在当前目录,为什么不把go_package="./",如果想存放在当前目录下的子目录,就go_package=“./subpkg “,上面这种做法,我无法理解,暂时就当做学了个参数用法吧,有大佬明白的可以留言。

--go_out=./:proto-gen-go插件编译产物的存放目录,这里是存放到当前目录,注意生成 的.pb.go文件的最终位置是你的--go_out=?位置+go_package=?位置,后者是在--go_out位置之后,进一步指定生成的.pb.go文件的存放路径。

-I ../:--proto_path=PATH的缩写

表示引入文件的目录路径,这里有坑。(这里如果看不懂,看到下面的import就明白了)

-I参数简单来说,就是如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I来指定搜索目录。如果没有指定-I参数,则在当前目录进行搜索。(这里的例子命令便是)

每个-I参数都引入一个目录,proto文件中引入了几个外部proto文件理论来说就需要多少个-I(同一目录的可以一次性引入),再加上待编译的proto也需要引入,所以上面这里就用了两个-I来引入目录文件。

--go_opt=moudle=....:protoc—gen-go插件的opt参数,采用go moudle模式.

hello.proto:proto文件路径。

1. syntax

表明使用proto3语法;如果你没有指定这个,编译器会使用proto2语法;这个指定语法行必须是文件的非空非注释的第一个行

2. 包(Package)

proto文件使用关键字package指定当前包名,类似于模块,定义proto包名,可以为.proto文件新增一个可选的package声明符作为生成语言的namespace,用来防止不同的消息类型有命名冲突.

3. 选项(Options)

在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。

在消息定义之前,可以通过option来进行配置,常用的option:

option go_package = "path;name";

path 表示生成的go文件的存放地址,会自动生成目录的。

name 表示生成的go文件所属的包名

4. 消息类型(message)

Protobuf中定义一个消息类型是通过关键字message字段指定的,这个关键字可以理解为Go语言的stuct关键字,用protobuf编译器将proto编译成Go代码之后,每个message都会生成一个名字与之对应的stuct结构体。

如上面的,就会生成一个名字为mmData的结构体。

变量(字段)的定义格式为:

[修饰符(可选)][数据类型][变量名(字段名)] = [唯一标识符] ;

其中唯一标识符是用来标识字段的,同一个message中字段的标识符不能相同。

1. 字段规则(字段修饰符)

message中的字段规则有三种。

required: 字段属性为必填字段。若不设置,则会导致编解码异常,导致消息被丢弃。

optional : 字段属性为可选字段。发送方可以选择性根据需要进行设置;

对于optional属性的字段,可以通过default关键字为字段设置默认值,即当发送方没有对该字段进行设置的时候,将使用默认值。

如果没有对字段设置默认值,就会根据特定的类型给字段赋予特定的默认值。

对于bool类型,默认值为false;对于string类型,默认值为空字符串;对于数值类型,默认值为0;对于枚举类型,默认值是枚举类型中的第一个值。

repeated : 字段属性为可重复字段,该字段可以包含[0,n]个元素,字段中的元素顺序被保留。类似于go的切片。

注意:

1.在proto3版本中,字段规则上移除了required,并把optional字段改名为singular。所有没有指定字段规则的字段默认为optional,对于为什么删除了require规则,参考:为什么 proto3 移除了 required 和 optional?

2.在proto2版本中,默认配置下,一个optional没有被设置或者被显示的设置为默认值,在序列化二进制格式的时候,这个字段将会被去掉,导致反序列化之后,无法区分当初没有设置还是设置了默认值,即使使用hasXXX()方法,对于设置的默认值的字段,也是返回false。解决方法:区分 Protobuf 中缺失值和默认值

2. 标识号(唯一标识符)

在消息体的定义中,每个字段都必须要有一个唯一的标识号。

这些标识号是用来在消息的二进制格式中识别各个字段的,一旦使用就不能再改变,否则会导致原有消息编解码出现异常。

标识号是[0,2^29 - 1]范围内的一个整数,其中**[19000,19999)之间的标识号在protobuf协议的实现中被预留了**,所以特写注意不要使用这个范围内的标识号,若使用进行编译的时候也会告警:

Field numbers 19000 through 19999 are reserved for the protocol buffer library implementation.

注意:

[1,15]内的标识号在编码的时候占用一个字节,[16,2047]之内的标识符占用两个字节,所以尽量为频繁使用的字段分配[1,15]内的标识号,另外预留出来一部分给未来可能频繁使用的字段。

3. 数据类型

3.1 基本数据类型

关于字段的默认值:

string类型的变量,默认值是空字符串

bytes类型的变量,默认值是空byte数组

bool类型的变量,默认值是false

数字类型的变量,默认值是0

枚举类型的变量,默认值是第一个枚举值,而且这个第一个枚举值的数字值必须是0

3.2 枚举类型

字段类型除了上述基本的字段类型之外,也可以是枚举类型。

syntax = "proto3";
package main;
option go_package = "./";
// 定义枚举类型
enum DayName {
  Sun  = 0;
  Mon  = 1;
  Tues = 2;
  Wed  = 3;
  Thur = 4;
  Fri  = 5;
  Sat  = 6;
}
message workDay {
  // 消息类型使用枚举类型
  optional DayName day = 1;
}

protoc --go_out=./ hello.proto生成的go文件里对应为const:

// 定义枚举类型
type DayName int32
const (
  DayName_Sun  DayName = 0
  DayName_Mon  DayName = 1
  DayName_Tues DayName = 2
  DayName_Wed  DayName = 3
  DayName_Thur DayName = 4
  DayName_Fri  DayName = 5
  DayName_Sat  DayName = 6
)
....
type WorkDay struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields
  // 消息类型使用枚举类型
  Day *DayName `protobuf:"varint,1,opt,name=day,proto3,enum=main.DayName,oneof" json:"day,omitempty"`
}

枚举常量的值必须在32位整数范围内,因为enum值是使用可编码方式存储的,对负数存储不够高效,因此不推荐在enum中使用负数。


枚举类型可以定义在message内,也可以定义在message外,若定义在message内,其他message要使用则需要通过messageType.enumType来进行引用。


默认情况下,枚举类型中的字段值不可重复,但是通过对enum添加option allow_alias = true;来达到对同一个枚举值起一个别名的目的,若不添加allow_alise并且有重复的枚举值编译的时候会报错。

syntax = "proto3";
package pkgName;
option go_package = "./";
// 定义枚举类型
enum DayName {
  // 若不添加该option,会报错:
  // "pkgName.Test" uses the same enum value as "pkgName.Sat".
  // If this is intended, set 'option allow_alias = true;' to the enum definition.
  option allow_alias = true;
  Sun                = 0;
  Mon                = 1;
  Tues               = 2;
  Wed                = 3;
  Thur               = 4;
  Fri                = 5;
  Sat                = 6;
  Test               = 6;  // Test与Sat字段值重名
}
3.3 map数据类型

除了上述类型之外,message还支持map类型。

syntax = "proto3";
package pkgName;
option go_package = "./";
message TData {
  map<int32, string> data = 1;
}

在生成的go文件对应map类型:

type TData struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields
  Data map[int32]string `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}

注意:

1.protobuf中的map实质上是无序的

2.proto中map类型不能用optional/required/repeated任何类型修饰。

3.4 message类型

protobuf允许将其他消息类型用作字段类型。

如下面userData中存在一个workDay类型的数据:

syntax = "proto3";
package pkgName;
option go_package = "./";
message workDay {
        int day = 1;
}
message userData {
        workDay userDays = 1;
}
3.5 嵌套消息类型

message可以无限嵌套

syntax = "proto3";
package pkgName;
option go_package = "./";
message OuterData1 {
  // 嵌套消息定义
  message TData {
    int32 a = 1;
  }
  // 引用嵌套消息
  TData            data1 = 1;
  OuterData2.TData data2 = 2;
}
message OuterData2 {
  // 嵌套消息定义
  message TData {
    int32 a = 1;
  }
}

在生成的hello.pb.go中对应:

type OuterData1 struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields
  // 引用嵌套消息
  Data1 *OuterData1_TData `protobuf:"bytes,1,opt,name=data1,proto3" json:"data1,omitempty"`
  Data2 *OuterData2_TData `protobuf:"bytes,2,opt,name=data2,proto3" json:"data2,omitempty"`
}
....
type OuterData2 struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields
}
3.6 Any字段(没看懂)
syntax = "proto3";
import "google/protobuf/any.proto";
package pkgName;
option go_package = "./";
message ErrorStatus {
  string                       message = 1;
  repeated google.protobuf.Any details = 21;
}
3.7 oneof 字段

如果你的 message 包含许多可选字段,并且最多只能同时设置其中一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。


Oneof 共享内存中的所有字段,并且最多只能同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。你可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 字段中当前是哪个值(如果有)被设置,具体方法取决于你选择的语言。


使用案例:

要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:

syntax = "proto3";
import "google/protobuf/any.proto";
package pkgName;
option go_package = "./";
message SampleMessage {
  oneof test_oneof {
    string name      = 1;
    string nike_name = 2;
  }
}

然后,将 oneof 字段添加到test_oneof的定义中。


你可以在test_oneof添加任何类型的字段,但不能使用 required,optional 或 repeated 关键字。如果需要向 oneof 添加重复字段,可以使用包含重复字段的 message。


在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。


在生成的hello.pb.go中为:

type SampleMessage struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields
  // Types that are assignable to TestOneof:
  //  *SampleMessage_Name
  //  *SampleMessage_NikeName
  TestOneof isSampleMessage_TestOneof `protobuf_oneof:"test_oneof"`
}
....
type SampleMessage_Name struct {
  Name string `protobuf:"bytes,1,opt,name=name,proto3,oneof"`
}
type SampleMessage_NikeName struct {
  NikeName string `protobuf:"bytes,2,opt,name=nike_name,json=nikeName,proto3,oneof"`
}
....

5. 定义服务(service)

如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将以你选择的语言生成服务接口和stub(桩)。


因此,例如,如果要定义一个 RPC 服务,其中包含一个根据 SearchRequest 返回 SearchResponse 的方法,可以在 .proto 文件中定义它,如下所示:

syntax = "proto3";
package pkgName;
option go_package = "./";
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
message SearchResponse {
  string result = 1;
}
service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

与 ProtoBuf 直接搭配使用的 RPC 系统是 gRPC :一个 Google 开发的平台无关语言无关的开源 RPC 系统。gRPC 和 ProtoBuf 能够非常完美的配合,你可以使用专门的 ProtoBuf 编译插件直接从.proto 文件生成相关 RPC 代码。

6. import导入其他proto文件

import

我们可以通过import导入其他proto文件,并使用该proto文件中的定义的消息类型。

---my_project
   |---protocol
       |---aaa
       |   |---aaa.proto
       |---bbb
           |---bbb.proto

aaa/aaa.proto

syntax = "proto3";
package aaa;
option go_package = "./";
message Something {
  string msg = 1;
}

bbb/bbb.proto

syntax = "proto3";
package bbb;
option go_package = "./";
import "aaa/aaa.proto";
message Something2 {
  aaa.Something something = 1;
}

虽然会报红但是不用管,生成 pb.go 的时候,假设当前在 my_project/protocol/bbb 目录下,则执行:

protoc -I ../ -I ./ --go_out=./ bbb.proto
# -I ../ : 在上一层目录中寻找引入的proto文件
# -I ./ : 在本层文件中找待编译的proto文件(顺序无所谓)

protoc有一个参数-I,表示引入文件的目录路径,这里有坑。

-I参数简单来说,就是如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I来指定搜索目录。如果没有指定-I参数,则在当前目录进行搜索。

每个-I参数都引入一个目录,proto文件中引入了几个外部proto文件理论来说就需要多少个-I(同一目录的可以一次性引入),再加上待编译的proto也需要引入,所以上面这里就用了两个-I来引入目录文件。


这样 protoc 可以在 -I path + import path => “./…/aaa/aaa.proto” 路径下找到 aaa.proto 这个文件。

# 当然也可以 import “aaa.proto”,-I=./…/aaa,同样可以执行成功。
protoc -I ../aaa -I ./ --go_out=./ bbb.proto


import public

默认情况下,proto只允许引用直接import的文件中定义的数据类型。


如b.proto中导入了a.proto,c.proto中导入了b.proto;默认情况下,c.proto中只能引用b.proto中定义的数据类型,而引用不到a.proto中的数据类型。若c.proto要使用a.proto中定义的数据类型,则b.proto引用a.proto的时候要使用import public。

---my_project
   |---protocol
       |---aaa
       |   |---aaa.proto
       |---bbb
           |---bbb.proto
       |---ccc
           |---ccc.proto

aaa/aaa.proto

syntax = "proto3";
package aaa;
option go_package = "./";
message Something {
  string msg = 1;
}

bbb/bbb.proto

syntax = "proto3";
package bbb;
option go_package = "./";
// import "aaa/aaa.proto"; 不加会报错
import public "aaa/aaa.proto";
message Something2 {
  aaa.Something something = 1;
}

ccc/ccc.proto

syntax = "proto3";
package ccc;
option go_package = "./";
import "bbb/bbb.proto";
message Something3 {
  aaa.Something something = 1;
}

执行:

protoc -I ../ -I ../ -I ./ --go_out=./ ccc.proto

这种用法在迁移proto文件到新的位置的时候十分有用,如Message类要从old.proto迁移到new.proto文件中,这个时候如果要在不修改对old.proto的文件的情况下,直接将Message移动到new.proto中,然后在old.proto中import public new.proto即可。

7. 更新Message消息类型原则

为了达到前后消息类型兼容的目的,扩展Message消息类型的时候需要注意一下几点:

1.不要更改任何已有的字段的数值标识。

2.所添加的字段属性必须是optional 或者repeated类型,如果扩展required类型,会导致旧的消息解析异常

3.非required字段可以移除。要保证它们的标示在新的消息类型中不再使用

4.一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。

5.int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。

6.sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。

7.string和bytes是兼容的——只要bytes是有效的UTF-8编码。

8.嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。

9.fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。

相关文章
|
3天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
|
2天前
|
存储 设计模式 安全
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
|
7天前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
7天前
|
Cloud Native 持续交付 云计算
云原生入门指南:从容器到微服务
【10月更文挑战第28天】在数字化转型的浪潮中,云原生技术成为推动现代软件开发的关键力量。本篇文章将带你了解云原生的基本概念,探索它如何通过容器化、微服务架构以及持续集成和持续部署(CI/CD)的实践来提升应用的可伸缩性、灵活性和可靠性。你将学习到如何利用这些技术构建和部署在云端高效运行的应用,并理解它们对DevOps文化的贡献。
25 2
|
10天前
|
Kubernetes 关系型数据库 MySQL
Kubernetes入门:搭建高可用微服务架构
【10月更文挑战第25天】在快速发展的云计算时代,微服务架构因其灵活性和可扩展性备受青睐。本文通过一个案例分析,展示了如何使用Kubernetes将传统Java Web应用迁移到Kubernetes平台并改造成微服务架构。通过定义Kubernetes服务、创建MySQL的Deployment/RC、改造Web应用以及部署Web应用,最终实现了高可用的微服务架构。Kubernetes不仅提供了服务发现和负载均衡的能力,还通过各种资源管理工具,提升了系统的可扩展性和容错性。
34 3
|
20天前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
20天前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
21天前
|
运维 Go 开发者
Go语言在微服务架构中的应用与优势
本文深入探讨了Go语言在构建微服务架构中的独特优势和实际应用。通过分析Go语言的核心特性,如简洁的语法、高效的并发处理能力以及强大的标准库支持,我们揭示了为何Go成为开发高性能微服务的首选语言。文章还详细介绍了Go语言在微服务架构中的几个关键应用场景,包括服务间通信、容器化部署和自动化运维等,旨在为读者提供实用的技术指导和启发。
|
9天前
|
监控 API 持续交付
后端开发中的微服务架构:从入门到精通
【10月更文挑战第26天】 在当今的软件开发领域,微服务架构已经成为了众多企业和开发者的首选。本文将深入探讨微服务架构的核心概念、优势以及实施过程中可能遇到的挑战。我们将从基础开始,逐步深入了解如何构建、部署和管理微服务。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的建议。
21 0
|
22天前
|
存储 安全 Go
Go语言切片:从入门到精通的深度探索###
本文深入浅出地剖析了Go语言中切片(Slice)这一核心概念,从其定义、内部结构、基本操作到高级特性与最佳实践,为读者提供了一个全面而深入的理解。通过对比数组,揭示切片的灵活性与高效性,并探讨其在并发编程中的应用优势。本文旨在帮助开发者更好地掌握切片,提升Go语言编程技能。 ###
下一篇
无影云桌面