前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言学习 - RPC篇:深入gRPC-Gateway-探索常用数据类型

Go语言学习 - RPC篇:深入gRPC-Gateway-探索常用数据类型

作者头像
junedayday
发布2022-12-02 19:14:41
1K0
发布2022-12-02 19:14:41
举报
文章被收录于专栏:Go编程点滴

概览

gRPC-Gateway的相关方案我们已经在上一篇详细描述。为了更方面地方便大家理解,我这边整理了一个最简化的git项目:https://github.com/Junedayday/grpc-gateway-buf-example/tree/v0.0.1

它主要包含两个特点:

  1. 用buf工具构建项目
  2. 同时启动了gRPC和gRPC-Gateway服务,支持两种协议的调用

今天,我们先迈出第一步:探索RPC服务中的数据类型。掌握常见的数据类型,灵活地运用到接口设计中,能帮助我们快速地提供优雅的接口类服务。

基础类型

protobuf的基础数据类型可参考链接:https://developers.google.com/protocol-buffers/docs/proto3#scalar

这部分属于是protobuf的基础知识,如果对这块不清楚,可以花5~10分钟快速过一下。

默认值问题

基础类型有一个很值得思考的问题:每一种基础类型都有一个默认值,如string的默认值为""int32的默认值是0。这就带来了一个问题:当一个字段被解析为默认值时,怎么区分是未传值,还是传的就是默认值

举个具体的例子,比如我们的传入参数为:

代码语言:javascript
复制
{
  "a":0,
  "b":1
}

代码语言:javascript
复制
{
  "b":1
}

我们将数据定义为

代码语言:javascript
复制
message Foo {
  int32 a = 1;
  int32 b = 2;
}

最终解析到Go结构体中的Foo.A字段都为0,但是,调用方对 未传值默认值 很可能有不同的定义。

这个问题有三种常规的解决思路:

  1. 利用编程语言特性,区分 未传值默认值 两种情况;
  2. 两边利用协议约定,保证未传值默认值等同;
  3. 新增加描述性字段,表明相关字段是否生效;

为了方便理解,我对上面三个case各举个例子:

方案1 - 在编程语言中区分

Go语言为例,会利用指针的特性,

代码语言:javascript
复制
type Foo struct {
 A *int32
 B *int32
}

在解析示例的json时,可以按如下方式进行区分:

  • 当为默认值0时,将A指向为0的指针
  • 当未传值时,将A指为nil

但是,这种实现对语言有一定要求:

  1. 要求语言支持指针(protobuf目标是跨语言的RPC方案)
  2. 对指针变量的操作需要不少额外的判断、转化操作

虽然方案1的普适性不高,但在Go语言的开源项目中很常见,比如各种共有云的Go SDK。

方案2 - 协议约定效果等同

方案2更多是一种内部约定。比如,定义了一个数据

代码语言:javascript
复制
message Book {
  int64 id = 1;
  string name = 2;
  float price = 3;
}

双方约定了:无论字段传的是默认值还是未传值,我们都按默认值处理。

但是,在接口中,我们会高频地复用数据结构。例如,Book这个数据结构在创建时没有问题,但将这个结构用在更新接口时,往往会有如下思路:

  • 如果是默认值,接口是希望将这个字段修改为默认值,如name为空
  • 如果未传值,接口是希望不更改这个字段,即不要修改name字段

所以,在方案2时,我们只能二选一:当遇到默认值时,要么认为是不改、要么认为是改成默认值。而如果要兼容,那就新增字段或者新增结构。

方案2虽然存在局限性,但是频率最高的使用方式:毕竟一般情况下调用方就几个,双方简单沟通一下就可以解决问题。但如果面向成百上千的调用方时,这个解释成本就很高了。

下面的方案3则是对其的一种演进:

方案3 - 新增加描述性字段

基于方案2,我们可以直接增加一个字段进行标识(类似于一种掩码的效果),如mask=["id","name"],表示:

  • id,name这两个字段生效
  • price字段不生效

这时,前面的问题就得以解决:

  • 如果希望修改name为空,mask中增加name字段
  • 如果不希望修改name,mask中不出现name字段

这个实现,就是Google推荐的FieldMask的实现思路,下面我们会再次说明。

枚举类型

protobuf的枚举的是一种可读性很强的定义,可以参考如下链接了解:https://developers.google.com/protocol-buffers/docs/proto3#enum

需要注意的是,官方推荐的将默认值0定义为XXX_UNSPECIFIED(即不在规定中,不具备实际意义),如

代码语言:javascript
复制
enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
}

它的实现思路与上面的方案2很像:规定默认值为未规定的,是一个无需关心的情况。这就要求使用方尽可能地使用非默认值的枚举值,减少歧义。

特殊类型

Any

代码语言:javascript
复制
import "google/protobuf/any.proto";

message ErrorStatus {
  repeated google.protobuf.Any details = 1;
}

Any可以简单理解为protobuf协议中的任意类型(但必须是由proto定义的)。我们可以从两个问题来理解它:

  • Any如何保证兼容性?
    • 内部将数据转化成了byte数组,就能存储任意数据了
  • Any如何解析到特定的proto结构?
    • 结合上面的byte数组和对应定义的proto文件

因此,传递的数据包含2个字段:

  • byte数组,表示具体数据
  • proto文件的定义,比如 "@type": "type.googleapis.com/junedayday.grpc_gateway_buf_example.echo_service.v1.EchoRequest"

但在实际场景中,Any使用并不方便,往往仅用在protobuf的内部协议中,不适合作为通用的API。

Oneof

代码语言:javascript
复制
message Book {
  oneof unique_id {
    int64 id = 1;
    string uuid = 2;
  }
}

Oneof适用的场景是多个字段中仅允许生效其中一个,这避免了理解上的冲突。例如,我们要查找书,每本书有2个唯一标识:iduuid

  • 如果传任意一个,我们能正常地查到
  • 如果同时传了iduuid,可能存在多种理解:
    • 同时根据两个条件查
    • 先根据id查,未查到再根据uuid查
    • 现根据uuid查,未查到再根据id查

从调用方来说,只能阅读你的接口文档,阅读各字段的注释。而Oneof字段呢,就在接口定义上直接告诉了你,二者只能选其一;如果你硬要传2个参数,就直接返回参数错误。

Oneof特性看起来很好用,但实际接口开发中的使用频率很低,毕竟通过有效的注释或者接口拆分,也能解决这个问题。

map

代码语言:javascript
复制
message EchoRequest {
  map<string, string> info = 1;
}

map是一个很常用的特性,定义和使用也十分简单。如示例,就会自动对应到Go语言中的map[string]string

但从API的设计来说,map这个容器有很高的扩展性,缺牺牲了一定的可读性,如key中代表的含义、有哪些限制等等,只能通过注释进行说明。

因此,map的特性要节制地使用,优先考虑用明确的结构定义来表示。

扩展类型

Value

代码语言:javascript
复制
import "google/protobuf/struct.proto";

message EchoRequest {
  google.protobuf.Value info = 1;
}

不同于AnyValue不需要依赖proto的定义,更趋近于通用意义上的泛型。它本质上是一种Oneof

代码语言:javascript
复制
message Value {
  oneof kind {
    NullValue null_value = 1;
    double number_value = 2;
    string string_value = 3;
    bool bool_value = 4;
    Struct struct_value = 5;
    ListValue list_value = 6;
  }
}

内部也提供了多个数据类型的转化,可按需调用,如GetXXXValue()

Struct

代码语言:javascript
复制
import "google/protobuf/struct.proto";

message EchoRequest {
  google.protobuf.Struct info = 1;
}

Strcut可快速对应到Go语言中的结构体,可以快速地转化为 map[string]structpb.Value。接下来的使用方式同上面的Value

FieldMask

代码语言:javascript
复制
import "google/protobuf/field_mask.proto";

message EchoRequest {
  google.protobuf.FieldMask field_mask = 1;
}

FieldMask就是上面基础类型中方案3的具体实现。它的定义很简单,就是一个字符串的数组:

代码语言:javascript
复制
message FieldMask {
  repeated string paths = 1;
}

这里面的每个元素,表示一个具体要生效的字段,支持多层的数据结构,如a.b

Duration

持续时间,需要一个数字+单位,如2s,减少了单位理解上的歧义。它由两个部分组成,很容易理解

代码语言:javascript
复制
message Duration {
  int64 seconds = 1;
  int32 nanos = 2;
}

TimeStamp

时间处理是一个很麻烦的方式,我们往往是采用string的方式传递、然后再次解析,相对来说比较折腾。

而官方提供了如下方式

代码语言:javascript
复制
import "google/protobuf/timestamp.proto";

message EchoRequest {
  google.protobuf.Timestamp time_stamp = 1;
}

我们可以利用AsTime()方法,快速地转化到Go语言中的time.Time结构,非常省力。对与输入方来说,时间要遵循 rfc3339 格式,如 2006-01-02T15:04:05Z

虽然我们更常用YYYY-MM-DD HH:mm:ss来表示,但rfc3339更具兼容性,建议尽可能地尝试替换。

小结

除了基础类型和枚举,我对今天谈到了8种类型进行了简单的概括:

数据类型

使用频率

可读性

Any

Oneof

map

Value

Struct

FieldMask

Duration

TimeStamp

同时,文中对默认值问题的分析,也希望能对大家在接口设计上有一定的启发。

Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go编程点滴 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概览
  • 基础类型
    • 默认值问题
      • 方案1 - 在编程语言中区分
        • 方案2 - 协议约定效果等同
          • 方案3 - 新增加描述性字段
          • 枚举类型
          • 特殊类型
            • Any
              • Oneof
                • map
                • 扩展类型
                  • Value
                    • Struct
                      • FieldMask
                        • Duration
                          • TimeStamp
                          • 小结
                          相关产品与服务
                          容器服务
                          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档