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端代码
//client
/* req1 := api.NoticeReaderRequest{
Msg: "我要睡觉咯",
NoticeWay: &api.NoticeReaderRequest_Email{
Email: "123456.qq.com",
},
}*/
req2 := api.NoticeReaderRequest{
Msg: "我要睡觉咯",
NoticeWay: &api.NoticeReaderRequest_Phone{
Phone: "12345678",
},
}
server端代码
//根据`NoticeWay`的不同而执行不同的操作
switch v := req.NoticeWay.(type) { //类型断言
case *api.NoticeReaderRequest_Email:
noticeWithEmail(v)
case *api.NoticeReaderRequest_Phone:
noticeWithPhone(v)
}
//.....
// 发送通知相关的功能函数
func noticeWithEmail(in *api.NoticeReaderRequest_Email) {
fmt.Printf("notice reader by email:%v\n", in.Email)
}
func noticeWithPhone(in *api.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代替基本类型。
在这个示例中,我们就可以使用Int64Value代替int64,修改后的protobuf文件如下。
message Book {
string title = 1;
string author = 2;
google.protobuf.Int64Value price = 3;
}
client端代码
使用了wrappers.proto中定义的包装类型后,我们在赋值的时候就需要额外包一层。
//client
import "google.golang.org/protobuf/types/known/wrapperspb"
book := api.Book{
Title: "学习书",
Author: "小王",
Price: &wrapperspb.Int64Value{Value: 99},
}
server端代码
WrapValue本质上类似于标准库sql中定义的sql.NullInt64、sql.NullString,即将基本数据类型包装为一个结构体类型。在使用时通过判断某个字段是否为nil(空指针)来区分该字段是否被赋值。
//server
if book.GetPrice() == nil { //没有给price赋值
fmt.Println("没有设置price")
} else {
fmt.Println(book.GetPrice().GetValue())
}
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的方式标识。
//client
import "google.golang.org/protobuf/types/known/fieldmaskpb"
paths := []string{"title", "price"}
req := api.UpdateBookRequest{
Op: "小王",
Book: &api.Book{
Title: "书",
Author: "小王",
Price: &wrapperspb.Int64Value{Value: 88},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
}
server端代码
在收到更新消息后,我们需要根据UpdateMask字段中记录的更新路径去读取更新数据。这里借助第三方库github.com/mennanov/fieldmask-utils实现。
//server
import "github.com/golang/protobuf/protoc-gen-go/generator"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"
mask, _ := fieldmask_utils.MaskFromProtoFieldMask(req.UpdateMask, generator.CamelCase)
var bookDst = make(map[string]interface{})
// 将数据读取到map[string]interface{}
// fieldmask-utils支持读取到结构体等,更多用法可查看文档。
fieldmask_utils.StructToMap(mask, req.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)