跳至主要內容

GRPC教程 5 - GRPC语法和常见包的使用

离心原创大约 7 分钟tutorialgolanggrpc

本篇文章,我会介绍一下gRPC常见的语法以及常见的包的调用,让大家快速入门gRPC,以便我们快速对gRPC的语法快速熟悉起来。

GRPC教程 5 - GRPC语法和常见包的使用

gRPC基础语法教程

官方教程open in new window

当然本期视频不会带着大家去仔细去过一遍官方文档,我们的主要目的是先run起来再说,先把最基础的学会了,然后基本就可以搞定60%的问题了,然后接着我们再对剩下的40%看一遍,有一个印象就可以了。

首先我们创建一个目录比如说就叫grpc-syntax-package

我们在这个目录下创建一个pb文件,用来存放我们的proto文件和生成的go文件。接着,我们再在里面创建一个person/person.proto,然后在里面写入

// person.proto
syntax = "proto3";

package person; // protobuf package

option go_package = "grpc-syntax-package/pb/person";

message Person {
  string Name = 1;
  int64 Age = 2;
}

然后呢?

我们就可以根据这个proto文件去生成对应的person.pb.go文件

protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative pb/person/person.proto 

这里--proto_path=pb的意思是,输入的proto文件是在当前目录下的pb目录

这里的--go_out=pb 的意思是,输出的.pb.go文件是在当前目录下的proto目录,前提是你得有proto目录,不然就会报错。

这里的-go_opt=paths=source_relative是最常见的一种输出文件的方式

所以相应的你也可以新创建一个proto目录去试试这个命令

protoc --proto_path=pb --go_out=proto --go_opt=paths=source_relative pb/person/person.proto

接着你可以点进对应的person.pb.go文件去对照proto文件看一下,

package student; 其实是protobuf的包,当别的proto文件去使用这个文件时,使用的就是这个包名
option go_package = "grpc-syntax-package/pb/person"; 这个的意思是 当在别的proto生成对应的文件时,输出引入的person的包就叫这个包。

接着我们创建一个student/student.proto去引入person.proto演示一下

syntax = "proto3";

package student;

option go_package = "grpc-syntax-package/pb/student";

import "pb/person/person.proto";

message Student {
  person.Person person = 1;
}

然后使用刚才的命令

protoc --proto_path=pb --go_out=proto --go_opt=paths=source_relative pb/person/person.proto pb/student/student.proto

发现是生成失败的,因为我们定义的--proto_path=pb是pb,而在student包里却是import "pb/person/person.proto";,所以找不到pb目录,我们需要修改一下命令

protoc --proto_path=. --go_out=. --go_opt=paths=source_relative pb/person/person.proto pb/student/student.proto

这样就可以了,让--proto_path=. --go_out=.就可以了。

这也是最简单的命令包使用。

我们来回顾一下。

使用protobuf编程的方式

1. 编写proto文件 
2. 输出go文件
3. 编写代码

接下来我们详细看一看message里的字段类型有哪些,这里不是要大家记住所有的字段,而是让大家有个印象,这样以后我们自己编写proto文件的时候,哪怕忘记了某个字段,但是有印象就可以去文档里找。

https://protobuf.dev/programming-guides/proto3/#scalaropen in new window

以及看看下面的默认类型。

Enumerations 枚举类型

enum Gender {
    Man = 0;
    Woman = 1;
  }
  Gender gender = 2;

Optional 可选类型

  optional uint32 age = 3;

Repeated 数组类型

  repeated string books = 4;

Reserved 保留类型, 当我们更新proto文件,比如需要删除某个字段时,但是可能有别的微服务在使用我们之前的proto版本,那我们可以直接删除某个字段吗?答案是不可以。这是因为在protobuf的二进制编码中,每个字段都有一个唯一的编号,用于标识该字段的类型和位置。如果您定义了一个新的字段并使用了已经保留的字段号,那么在序列化和反序列化时就会出现冲突,导致数据解析失败。

因此,当您使用 reserved 关键字时,编译器会检查您定义的每个字段的编号是否与已保留的字段号冲突。如果您尝试使用保留的字段号定义新的字段,编译器会报错,以防止在序列化和反序列化时出现数据损坏或解析失败的情况。

  reserved 5;

Map 映射类型

  map<string, string> m = 6;

oneof 字段

oneof content {
int32 number = 7;
string text = 8;
bool flag = 9;
}

于是我们的代码后可以这样

p := pb.Person{
		Name:   "John Smith",
		Age:    proto.Uint32(32),
		Gender: pb.Person_Woman,
		Books:  []string{"book1", "book2"},
		M: map[string]string{
			"key1": "value1",
			"key2": "value2",
		},
		Content: &pb.Person_Text{Text: "221dd"},
	}

gRPC导入别的包去使用

any包

导入

Import "google.golang.org/protobuf/types/known/anypb"


google.protobuf.Any details = 10;

生成代码

然后代码中我们怎么使用呢?

Details: &anypb.Any{
			TypeUrl: "example.com/MyMessageDetails",
			Value: []byte("your details"),
		},

timestamp包

在 Protobuf 中,google.protobuf.Timestamp 类型表示一个时间戳,它包含了秒数和纳秒数两个字段。在 Go 中,google.protobuf.Timestamp 被映射为 *timestamp.Timestamp 类型。

import	"google.golang.org/protobuf/types/known/timestamppb"

now := time.Now()
timestampProto := timestamppb.New(now)
CreatedAt: timestampProto,

field_mask 包

在 Protobuf 中,google.protobuf.FieldMask 类型用于指定在更新一个资源时需要更新哪些字段。它包含一个字段列表,列表中的每个字段表示需要更新的一个字段的路径。

我们新加入一个message,来请求更新我们的Person

message UpdatePersonReq {
  string name = 1;
  google.protobuf.FieldMask update_mask = 2;
  Person update_data = 3;
}
import "google/protobuf/field_mask.proto";


	mask := &fieldmaskpb.FieldMask{
		Paths: []string{"age", "gender"},
	}

	req := &pb.UpdatePersonReq{
		Name: "John Smith",
		UpdateMask: mask,
		UpdateData: &pb.Person{
			Name: "Lixin",
			Gender: pb.Person_Woman,
			Content: &pb.Person_Flag{Flag: true},
		},
	}

如果我们的客户端这么写,服务器端怎么根据field_mask值去更新对应的字段呢?


func resp(req pb.UpdatePersonReq) {
	// 首先,我们需要检查请求中是否指定了 update_mask 字段
	if req.UpdateMask == nil {
			// 如果 update_mask 字段未指定,则更新所有字段
			return db.UpdateResource(req.Id, req.UpdatedData)
	}

	// 如果 update_mask 字段指定了,则只更新指定的字段
	// 首先,我们需要将 FieldMask 转换为一个字符串数组
	paths := req.UpdateMask.Paths

	// 然后,我们需要创建一个包含要更新的字段的 map
	fields := make(map[string]interface{})
	for _, path := range paths {
			switch path {
			case "name":
					fields[path] = req.UpdateData.Name
			case "gender":
					fields[path] = req.UpdateData.Gender
			// 如果请求中包含了我们不支持的字段,则返回一个错误
			default:
					return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Unsupported field: %s", path))
			}
	}

	// 最后,我们调用数据库的 UpdateResourceWithFields 方法来更新指定的字段
	return db.UpdateResourceWithFields(req.Name, fields)
}

可以我们的服务端可以类似这样的去写,根据field_mask字段去更新对应的字段信息,你可能会问了,为什么直接传递一个Person?因为使用 FieldMask 字段的主要目的是为了避免在更新操作中无意中修改了一些不应该被修改的字段,从而导致不可预料的后果。如果我们误传了一个错误的 Person 对象,甚至可能会更新一些我们根本不想更新的字段,从而导致数据不一致等问题。

使用 FieldMask 字段可以让客户端明确地指定要更新的字段,从而避免这些问题。通过指定 FieldMask 字段,客户端可以控制更新操作仅修改指定的字段,而不是所有字段。这样,即使客户端传递了错误的数据,服务器也可以根据 FieldMask 中指定的字段来执行正确的操作,从而避免不必要的风险。

因此,使用 FieldMask 字段是一个很好的实践,可以帮助我们编写更加健壮和可靠的代码。

总结

我们在本期教程中,第一部分,学习了gRPC的主要语法以及如何用protoc工具生成.pb.go文件以及生成对应的_grpc.pb.go文件和相关的命令行的作用。认识到了proto里面的某些字段的作用比如package、go_package的作用。以及定义message、定义service服务。

第二部分,我们主要学习了protobuf中的三个最常见的包的使用包括timestamp,any,field_mask。希望本期视频后,大家都能够自己动手试一试,自己敲一敲代码对这些熟悉起来。