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.NullInt64
、sql.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)
參考資料:
- https://cloud.google.com/apis/design/standard_methods
- https://github.com/mennanov/fieldmask-utils
- https://github.com/iancoleman/strcase
交流q裙:579480724