乐趣区

关于protobuf:区分Protobuf-3中缺失值和默认值

来自公众号:新世界杂货铺

这两天翻了翻以前的我的项目,发现不同我的项目中对于 Protobuf 3 缺失值和默认值的辨别竟然有好几种实现。明天笔者冷饭新炒,联合我的项目中的实现以及切身经验共总结出如下六种计划。

减少标识字段

家喻户晓,在 Go 中数字类型的默认值为0(这里仅以数字类型举例),这在某些场景下往往会引起肯定的歧义。

is_show 字段为例,如果没有该字段示意不更新 DB 中的数据,如果有该字段且值为 0 则示意更新 DB 中的数据为不可见,如果有该字段且值为 1 则示意更新 DB 中的数据为可见。

上述场景中,理论要解决的问题是如何辨别默认值和缺失字段。减少标识字段是通过额定减少一个字段来达到辨别的目标。

例如:减少一个 has_show_field 字段标识 is_show 是否为有效值。如果 has_show_fieldtrueis_show 为有效值,否则认为 is_show 未设置值。

此计划尽管直白,但每次设置 is_show 的值时还需设置 has_show_field 的值,甚是麻烦故笔者非常不举荐。

字段含意和默认值辨别

字段含意和默认值辨别即不应用对应类型的默认值作为该字段的有效值。接着后面的例子持续形容,is_show为 1 时示意展现,is_show为 2 时示意不展现,其余状况则认为 is_show 未设置值。

此计划笔者还是比拟认可的,惟一问题就是和开发者的默认习惯稍微不符。

应用 oneof

oneof 的用意是达到 C 语言 union 数据类型的成果,然而诸多大佬还是发现它能够标识缺失字段。

message Status {
  oneof show {int32 is_show = 1;}
}
message Test {
    int32 bar = 1;
    Status st = 2;
}

上述 proto 文件生成对应 go 文件后,Test.StStatus 的指针类型,故通过此计划能够辨别默认值和缺失字段。然而笔者认为此计划做 json 序列化时非常不敌对,上面是笔者的例子:

// oneof to json
ot1 := oneof.Test{
  Bar: 1,
  St: &oneof.Status{
    Show: &oneof.Status_IsShow{IsShow: 1,},
  },
}
bts, err := json.Marshal(ot1)
fmt.Println(string(bts), err)
// json to oneof failed
jsonStr := `{"bar":1,"st":{"Show":{"is_show":1}}}`
var ot2 oneof.Test
fmt.Println(json.Unmarshal([]byte(jsonStr), &ot2))

上述输入后果如下:

{"bar":1,"st":{"Show":{"is_show":1}}} <nil>
json: cannot unmarshal object into Go struct field Status.st.Show of type oneof.isStatus_Show

通过上述输入知,oneof 的 json.Marshal 输入后果会额定多一层,而 json.Unmarshal 还会失败,因而应用 oneof 时需谨慎。

应用 wrapper 类型

这应该是 google 官网提出的解决方案,咱们看看上面的例子:

import "google/protobuf/wrappers.proto";
message Status {google.protobuf.Int32Value is_show = 1;}
message Test {
    int32 bar = 1;
    Status st = 2;
}

应用此计划须要引入 google/protobuf/wrappers.proto。此计划生成对应 go 文件后,Test.St 也是 Status 的指针类型。同样,咱们也看一下它的 json 序列化成果:

wra1 := wrapper.Test{
  Bar: 1,
  St: &wrapper.Status{IsShow: wrapperspb.Int32(1),
  },
}
bts, err = json.Marshal(wra1)
fmt.Println(string(bts), err)
jsonStr = `{"bar":1,"st":{"is_show":{"value":1}}}`
// 可失常转 json
var wra2 wrapper.Test
fmt.Println(json.Unmarshal([]byte(jsonStr), &wra2))

上述输入后果如下:

{"bar":1,"st":{"is_show":{"value":1}}} <nil>
<nil>

和 oneof 计划相比 wrapper 计划的 json 反序列化是没问题的,然而 json.Marshal 的输入后果也会额定多一层。另外,经笔者在本地试验,此计划无奈和 gogoproto 一起应用。

容许 proto3 应用 optional 标签

后面几个计划预计在实践中还是不够尽如人意。于是 2020 年 5 月 16 日 protoc v3.12.0 公布,该编译器容许 proto3 的字段也可应用 optional润饰。

上面看看例子:

message Status {optional int32 is_show = 1;}
message Test {
    int32 bar = 1;
    Status st = 2;
}

此计划须要应用新版本的 protoc 且必须应用 --experimental_allow_proto3_optional 开启此个性。protoc 降级教程见 https://github.com/protocolbu…。上面持续看看该计划的 json 序列化成果

var isShow int32 = 1
p3o1 := p3optional.Test{
  Bar: 1,
  St:  &p3optional.Status{IsShow: &isShow},
}
bts, err = json.Marshal(p3o1)
fmt.Println(string(bts), err)
var p3o2 p3optional.Test
jsonStr = `{"bar":1,"st":{"is_show":1}}`
fmt.Println(json.Unmarshal([]byte(jsonStr), &p3o2))

上述输入后果如下:

{"bar":1,"st":{"is_show":1}} <nil>
<nil>

据上述后果知,此计划与 oneof 以及 wrapper 计划的 json 序列化相比更加合乎预期,同样,经笔者在本地试验,此计划无奈和 gogoproto 一起应用。

proto2 和 proto3 联合应用

作为一个 gogoproto 的忠诚用户,笔者心愿在能辨别默认值和缺失值的同时还能够持续应用 gogoproto 的个性。于是便产生了 proto2 和 proto3 联合应用的野路子。

// proto2
message Status {optional int32 is_show = 2;}
// proto3
message Test {int32 bar = 1 [(gogoproto.moretags) = 'form:"more_bar"', (gogoproto.jsontag) ='custom_tag'];
    p3p2.Status st = 2;
}

须要辨别缺失字段和默认值的 message 定义在语法为 proto2 的文件中,proto3 通过 import 导入 proto2 的 message 以达辨别目标。

optional润饰的字段在 Go 中会生成指针类型,因而辨别缺失值和默认值就变的非常容易了。上面看看此计划的 json 序列化成果:

// p3p2 to json
p3p21 := p3p2.Test{
  Bar: 1,
  St:  &p3p2.Status{IsShow: &isShow},
}
bts, err = json.Marshal(p3p21)
fmt.Println(string(bts), err)
var p3p22 p3p2.Test
jsonStr = `{"custom_tag":1,"st":{"is_show":1}}`
fmt.Println(json.Unmarshal([]byte(jsonStr), &p3p22))

上述输入后果如下:

{"custom_tag":1,"st":{"is_show":1}} <nil>
<nil>

根据上述后果知,此计划不仅可能活用 gogoproto 的各种 tag,其后果也和 在 proto3 中间接应用 optional成果统一。尽管笔者曾经在本人的我的项目中应用了此计划,然而依然要揭示一句:“写本篇文章时,笔者特意去 github 看了 gogoproto 的公布日志,gogoproto 最新一个版本公布工夫为2019 年 10 月 14 日,笔者大胆预言 gogoproto 当前不会再更新了,所以此计划还请大家酌情应用”。

最初,衷心希望本文可能对各位读者有肯定的帮忙。

注:

  1. 文中笔者所用 go 版本为:go1.15.2
  2. 文中笔者所用 protoc 版本为:3.14.0
  3. 文章中所用残缺例子:https://github.com/Isites/go-…
退出移动版