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

protobuf中使用oneof、WrapValue和FieldMask

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

oneof

如果你有一條包含多個字段的消息,並且最多同時設置其中一個字段,那麼你可以通過使用oneof來實現並節省內存。

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

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

protobuf 定義

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

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

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

client端代碼

Go語言創建oneof字段的client端示例代碼。

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

server端代碼

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

// ... liwenzhou.com ...

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

// ... liwenzhou.com ...

// 發送通知相關的功能函數
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又加回來了),這使得我們沒辦法輕易判斷某些字段究竟是未賦值還是其被賦值爲零值。

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

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

protobuf 定義

類似這種場景推薦使用google/protobuf/wrappers.proto中定義的WrapValue,本質上就是使用自定義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: "《跟七米學Go語言》",
    Price: &wrapperspb.Int64Value{Value: 9900},
}

server端代碼

WrapValue本質上類似於標準庫sql中定義的sql.NullInt64sql.NullString,即將基本數據類型包裝爲一個結構體類型。在使用時通過判斷某個字段是否爲nil(空指針)來區分該字段是否被賦值。

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標識price爲可選字段。

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

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

client端代碼

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

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

book := proto.Book{
    Title: "《跟七米學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

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

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

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

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

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

爲了實現一個支持部分更新的接口,我們把UpdateBookRequest消息修改如下。

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

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

client端代碼

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

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

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

server端代碼

在收到更新消息後,我們需要根據UpdateMask字段中記錄的更新路徑去讀取更新數據。這裏藉助第三方庫github.com/mennanov/fieldmask-utils實現。

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{})
// 將數據讀取到map[string]interface{}
// fieldmask-utils支持讀取到結構體等,更多用法可查看文檔。
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函數(簽名如下)

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

接收的naming參數本質上是一個將字段掩碼字段名映射到 Go 結構中使用的名稱的函數,它必須根據你的實際需求實現。

例如在我們這個示例中,還可以使用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{})
// 將數據讀取到map[string]interface{}
// fieldmask-utils支持讀取到結構體等,更多用法可查看文檔。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDst
fmt.Printf("bookDst:%#v\n", bookDst)

參考資料:

交流q裙:579480724

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章