區(qū)分Protobuf 3中缺失值和默認(rèn)值

來(lái)自公#眾#號(hào):新世界雜貨鋪

這兩天翻了翻以前的項(xiàng)目,發(fā)現(xiàn)不同項(xiàng)目中關(guān)于Protobuf 3缺失值和默認(rèn)值的區(qū)分居然有好幾種實(shí)現(xiàn)。今天筆者冷飯新炒,結(jié)合項(xiàng)目中的實(shí)現(xiàn)以及切身經(jīng)驗(yàn)共總結(jié)出如下六種方案。

增加標(biāo)識(shí)字段

眾所周知,在Go中數(shù)字類型的默認(rèn)值為0(這里僅以數(shù)字類型舉例),這在某些場(chǎng)景下往往會(huì)引起一定的歧義。

is_show字段為例,如果沒(méi)有該字段表示不更新DB中的數(shù)據(jù),如果有該字段且值為0則表示更新DB中的數(shù)據(jù)為不可見(jiàn),如果有該字段且值為1則表示更新DB中的數(shù)據(jù)為可見(jiàn)。

上述場(chǎng)景中,實(shí)際要解決的問(wèn)題是如何區(qū)分默認(rèn)值和缺失字段。增加標(biāo)識(shí)字段是通過(guò)額外增加一個(gè)字段來(lái)達(dá)到區(qū)分的目的。

例如:增加一個(gè)has_show_field字段標(biāo)識(shí)is_show是否為有效值。如果has_show_fieldtrueis_show為有效值,否則認(rèn)為is_show未設(shè)置值。

此方案雖然直白,但每次設(shè)置is_show的值時(shí)還需設(shè)置has_show_field的值,甚是麻煩故筆者十分不推薦。

字段含義和默認(rèn)值區(qū)分

字段含義和默認(rèn)值區(qū)分即不使用對(duì)應(yīng)類型的默認(rèn)值作為該字段的有效值。接著前面的例子繼續(xù)描述,is_show為1時(shí)表示展示,is_show為2時(shí)表示不展示,其他情況則認(rèn)為is_show未設(shè)置值。

此方案筆者還是比較認(rèn)可的,唯一問(wèn)題就是和開(kāi)發(fā)者的默認(rèn)習(xí)慣略微不符。

使用oneof

oneof 的用意是達(dá)到 C 語(yǔ)言 union 數(shù)據(jù)類型的效果,但是諸多大佬還是發(fā)現(xiàn)它可以標(biāo)識(shí)缺失字段。

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

上述proto文件生成對(duì)應(yīng)go文件后,Test.StStatus的指針類型,故通過(guò)此方案可以區(qū)分默認(rèn)值和缺失字段。但是筆者認(rèn)為此方案做json序列化時(shí)十分不友好,下面是筆者的例子:

// 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))

上述輸出結(jié)果如下:

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

通過(guò)上述輸出知,oneof的json.Marshal輸出結(jié)果會(huì)額外多一層,而json.Unmarshal還會(huì)失敗,因此使用oneof時(shí)需謹(jǐn)慎。

使用wrapper類型

這應(yīng)該是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。此方案生成對(duì)應(yīng)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}}}`
// 可正常轉(zhuǎn)json
var wra2 wrapper.Test
fmt.Println(json.Unmarshal([]byte(jsonStr), &wra2))

上述輸出結(jié)果如下:

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

和oneof方案相比wrapper方案的json反序列化是沒(méi)問(wèn)題的,但是json.Marshal的輸出結(jié)果也會(huì)額外多一層。另外,經(jīng)筆者在本地試驗(yàn),此方案無(wú)法和gogoproto一起使用。

允許proto3使用optional標(biāo)簽

前面幾個(gè)方案估計(jì)在實(shí)踐中還是不夠盡善盡美。于是2020年5月16日protoc v3.12.0發(fā)布,該編譯器允許proto3的字段也可使用 optional修飾。

下面看看例子:

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

此方案需要使用新版本的protoc且必須使用--experimental_allow_proto3_optional開(kāi)啟此特性。protoc升級(jí)教程見(jiàn)https://github.com/protocolbuffers/protobuf#protocol-compiler-installation。下面繼續(xù)看看該方案的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))

上述輸出結(jié)果如下:

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

據(jù)上述結(jié)果知,此方案與oneof以及wrapper方案的json序列化相比更加符合預(yù)期,同樣,經(jīng)筆者在本地試驗(yàn),此方案無(wú)法和gogoproto一起使用。

proto2和proto3結(jié)合使用

作為一個(gè)gogoproto的忠實(shí)用戶,筆者希望在能區(qū)分默認(rèn)值和缺失值的同時(shí)還可以繼續(xù)使用gogoproto的特性。于是便產(chǎn)生了proto2和proto3結(jié)合使用的野路子。

// 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;
}

需要區(qū)分缺失字段和默認(rèn)值的message定義在語(yǔ)法為proto2的文件中,proto3通過(guò)import導(dǎo)入proto2的message以達(dá)區(qū)分目的。

optional修飾的字段在Go中會(huì)生成指針類型,因此區(qū)分缺失值和默認(rèn)值就變的十分容易了。下面看看此方案的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))

上述輸出結(jié)果如下:

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

根據(jù)上述結(jié)果知,此方案不僅能夠活用gogoproto的各種tag,其結(jié)果也和在proto3中直接使用optional效果一致。雖然筆者已經(jīng)在自己的項(xiàng)目中使用了此方案,但是仍然要提醒一句:“寫本篇文章時(shí),筆者特意去github看了gogoproto的發(fā)布日志,gogoproto最新一個(gè)版本發(fā)布時(shí)間為2019年10月14日,筆者大膽預(yù)言gogoproto以后不會(huì)再更新了,所以此方案還請(qǐng)大家酌情使用”。

最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。

注:

  1. 文中筆者所用go版本為:go1.15.2
  2. 文中筆者所用protoc版本為:3.14.0
  3. 文章中所用完整例子:https://github.com/Isites/go-coder/blob/master/pbjson/main.go
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容