【golang微服務(wù)】protobuf中oneof、WrapValue和FieldMask的使用

protobuf中使用oneof、WrapValue和FieldMask

本文介紹了在Go語言中如何使用oneof字段以及如何通過使用google/protobuf/wrappers.proto中定義的類型區(qū)分默認值和沒有傳值;最后演示了Go語言中借助fieldmask-utils庫使用google/protobuf/field_mask.proto實現(xiàn)部分更新的方法。

oneof

如果你有一條包含多個字段的消息,并且最多同時設(shè)置其中一個字段,那么你可以通過使用oneof來實現(xiàn)并節(jié)省內(nèi)存。

oneof字段類似于常規(guī)字段,只不過oneof中的所有字段共享內(nèi)存,而且最多可以同時設(shè)置一個字段。設(shè)置其中的任何成員都會自動清除所有其他成員。

可以在oneof中添加除了map字段和repeated字段外的任何類型的字段。

protobuf 定義

假設(shè)我的博客系統(tǒng)支持為讀者朋友們發(fā)送博客更新的通知信息,系統(tǒng)支持通過郵件和短信兩個方式發(fā)送通知。但每一次只允許使用一種方式發(fā)送通知。

在這個場景下我們就可以使用oneof字段來定義通知的方式——notice_way

// 通知讀者的消息
message NoticeReaderRequest{
    string msg = 1;
    oneof notice_way{
        string email = 2;
        string phone = 3;
    }
}

client端代碼

Go語言創(chuàng)建oneof字段的client端示例代碼。

// 使用郵件通知的請求消息
noticeReq := proto.NoticeReaderRequest{
    Msg: "李文周的博客更新啦~",
    NoticeWay: &proto.NoticeReaderRequest_Email{
        Email: "123@xx.com",
    },
}
// 使用短信通知的請求消息
noticeReq2 := proto.NoticeReaderRequest{
    Msg: "李文周的博客更新啦~",
    NoticeWay: &proto.NoticeReaderRequest_Phone{
        Phone: "123456789",
    },
}

server端代碼

Go語言操作oneof字段的server端示例代碼。下面的代碼中使用switch case的方式,根據(jù)請求消息中的通知類型選擇執(zhí)行不同的業(yè)務(wù)邏輯。

// ... liwenzhou.com ...

// 根據(jù)`NoticeWay`的不同而執(zhí)行不同的操作
switch v := noticeReq.NoticeWay.(type) {
case *proto.NoticeReaderRequest_Email:
    noticeWithEmail(v)
case *proto.NoticeReaderRequest_Phone:
    noticeWithPhone(v)
}

// ... liwenzhou.com ...

// 發(fā)送通知相關(guān)的功能函數(shù)
func noticeWithEmail(in *proto.NoticeReaderRequest_Email) {
    fmt.Printf("notice reader by email:%v\n", in.Email)
}

func noticeWithPhone(in *proto.NoticeReaderRequest_Phone) {
    fmt.Printf("notice reader by phone:%v\n", in.Phone)
}

WrapValue

protobuf v3在刪除required的同時把optional也一起刪除了(v3.15.0又加回來了),這使得我們沒辦法輕易判斷某些字段究竟是未賦值還是其被賦值為零值。

例如,當(dāng)我們有如下消息定義時,我們拿到一個book消息,當(dāng)book.Price = 0時我們沒辦法區(qū)分book.Price字段是未賦值還是被賦值為0。

message Book {
    string title = 1;
    string author = 2;
    int64 price = 3;
}

protobuf 定義

類似這種場景推薦使用google/protobuf/wrappers.proto中定義的WrapValue,本質(zhì)上就是使用自定義message代替基本類型。

// google/protobuf/wrappers.proto

// ...

// Wrapper message for `float`.
//
// The JSON representation for `FloatValue` is JSON number.
message FloatValue {
  // The float value.
  float value = 1;
}

// Wrapper message for `int64`.
//
// The JSON representation for `Int64Value` is JSON string.
message Int64Value {
  // The int64 value.
  int64 value = 1;
}

// ... 

在這個示例中,我們就可以使用Int64Value代替int64,修改后的protobuf文件如下。

message Book {
    string title = 1;
    string author = 2;
    google.protobuf.Int64Value price = 3;
}

client端代碼

使用了wrappers.proto中定義的包裝類型后,我們在賦值的時候就需要額外包一層。

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

book := proto.Book{
    Title: "《跟七米學(xué)Go語言》",
    Price: &wrapperspb.Int64Value{Value: 9900},
}

server端代碼

WrapValue本質(zhì)上類似于標(biāo)準庫sql中定義的sql.NullInt64sql.NullString,即將基本數(shù)據(jù)類型包裝為一個結(jié)構(gòu)體類型。在使用時通過判斷某個字段是否為nil(空指針)來區(qū)分該字段是否被賦值。

if book.GetPrice() == nil {  // price沒賦值
    fmt.Println("book with no price")
} else {
    fmt.Printf("book with price:%v\n", book.GetPrice().GetValue())
}

v3.15.0+使用optional

Protobuf v3.15.0 版本之后又支持使用optional顯式指定字段為可選。

下面的示例中,我們使用optional標(biāo)識price為可選字段。

message Book {
    string title = 1;
    string author = 2;
    //google.protobuf.Int64Value price = 3;
    optional int64 price = 3;  // 使用optional
}

修改了proto文件后,重新編譯。

client端代碼

現(xiàn)在price字段就是*int64類型了,我們需要使用google.golang.org/protobuf/proto包提供的系列函數(shù)完成賦值操作。

import "google.golang.org/protobuf/proto"

book := proto.Book{
    Title: "《跟七米學(xué)Go語言》",
    Price: proto.Int64(9900),
}

server端代碼

如果需要判斷price字段是否賦值,可以判斷是否為nil。

if book.Price == nil {  // price沒賦值
    fmt.Println("book with no price")
} else {
    fmt.Printf("book with price:%v\n", book.GetPrice())
}

FieldMask

假設(shè)現(xiàn)在需要實現(xiàn)一個更新書籍信息接口,我們可能會定義如下更新書籍的消息。

message UpdateBookRequest {
    // 操作人 
    string op = 1;
    // 要更新的書籍信息
    Book book = 2;
}

但是如果我們的Book中定義有很多很多字段時,我們不太可能每次請求都去全量更新Book的每個字段,因為通常每次操作只會更新1到2個字段。

那么我們該如何確定每次更新操作涉及到了哪些具體字段呢?

答案是使用google/protobuf/field_mask.proto,它能夠記錄在一次更新請求中涉及到的具體字段路徑。

為了實現(xiàn)一個支持部分更新的接口,我們把UpdateBookRequest消息修改如下。

message UpdateBookRequest {
    // 操作人 
    string op = 1;
    // 要更新的書籍信息
    Book book = 2;

    // 要更新的字段
    google.protobuf.FieldMask update_mask = 3;
}

client端代碼

我們通過paths記錄本次更新的字段路徑,如果是嵌套的消息類型則通過x.y的方式標(biāo)識。

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

paths := []string{"title", "read"} // 記錄更新的字段路徑
updateReq := proto.UpdateBookRequest{
    Book: &proto.Book{
        Title: "《跟七米學(xué)Go語言》",
        Read:  true,
    },
    UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
}

server端代碼

在收到更新消息后,我們需要根據(jù)UpdateMask字段中記錄的更新路徑去讀取更新數(shù)據(jù)。這里借助第三方庫github.com/mennanov/fieldmask-utils實現(xiàn)。

import "github.com/golang/protobuf/protoc-gen-go/generator"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"

mask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, generator.CamelCase)
var bookDst = make(map[string]interface{})
// 將數(shù)據(jù)讀取到map[string]interface{}
// fieldmask-utils支持讀取到結(jié)構(gòu)體等,更多用法可查看文檔。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDst
fmt.Printf("bookDst:%#v\n", bookDst)

2022-11-20更新:由于github.com/golang/protobuf/protoc-gen-go/generator包已棄用,而MaskFromProtoFieldMask函數(shù)(簽名如下)

func MaskFromProtoFieldMask(fm *field_mask.FieldMask, naming func(string) string) (Mask, error)

接收的naming參數(shù)本質(zhì)上是一個將字段掩碼字段名映射到 Go 結(jié)構(gòu)中使用的名稱的函數(shù),它必須根據(jù)你的實際需求實現(xiàn)。

例如在我們這個示例中,還可以使用github.com/iancoleman/strcase包提供的ToCamel方法:

import "github.com/iancoleman/strcase"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"

mask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, strcase.ToCamel)
var bookDst = make(map[string]interface{})
// 將數(shù)據(jù)讀取到map[string]interface{}
// fieldmask-utils支持讀取到結(jié)構(gòu)體等,更多用法可查看文檔。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDst
fmt.Printf("bookDst:%#v\n", bookDst)

參考資料:

交流q裙:579480724

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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