gRPC FieldMask类型及其使用

项目中使用proto定义接口时,使用到了FieldMask类型。本文将对FieldMask类型的适用场景及方法做一些整理。

关于FieldMask类型

FieldMask类型定义于google/protobuf/field_mask.proto中,是用于描述属性路径的集合。通常用于指定获取请求要返回的字段集合,或者更新请求要修改的字段集合。

FieldMask使用

  1. proto中定义
message FieldmaskTestReq {
  string msg = 1;
  Nested nested = 2;
  google.protobuf.FieldMask field_mask = 3;
}
  1. 调用方使用

使用RPC调用方式,需要在客户端调用时显示指定FieldMask,代码如下:

fm, err := fieldmaskpb.New(req, "msg")
if err != nil {
	log.Printf("construct field_mask error: %+v", err)
	return err
}
req.FieldMask = fm
  1. 服务端代码
if req.FieldMask != nil {
	mask, err := fieldmaskutils.MaskFromPaths(req.FieldMask.Paths, func(s string) string {
		s = strings.Replace(s, "_", " ", -1)
		s = strings.Title(s)
		return strings.Replace(s, " ", "", -1)
	})
	if err != nil {
		log.Printf("mask error: %+v", err)
		return nil, err
	}
	fieldmaskutils.StructToStruct(mask, full.Nested, res.Nested)
}

服务端代码中,需要使用"github.com/mennanov/fieldmask-utils"提供的MaskFromPaths函数,对于http请求,需要将FieldMask结构体中path中存储的属性值由蛇形转换为go结构体中对应的大驼峰格式。关于HTTP下FieldMask的使用,后续将有介绍。

何时使用FieldMask

关于FieldMask的使用场景,在protobuf官方文档中已有说明如下:

// Field masks are used to specify a subset of fields that should be
// returned by a get operation or modified by an update operation.
// Field masks also have a custom JSON encoding (see below).

主要有两个场景:

  1. 指定查询接口返回的字段
  2. 指定需更新字段

关于场景1,有一篇Netflix的帖子做了更详细的介绍,原文链接为Practical API Design at Netflix, Part 1: Using Protobuf FieldMask。InfoQ有一篇译文介绍为Netflix实用API设计第1部分:使用Protobuf FieldMask。服务端的核心实现为使用类似于fieldmaskutils.StructToStruct的方法,根据FieldMask去构造Response字段。

关于场景2,当使用FieldMask表示需要更新的字段时,需要在response中显式判断FieldMask.Path中传入的值,并根据传入值处理数据。个人观点,认为此场景下使用Wrapper进行处理更为方便,可避免很多侵入的判断代码。

FieldMask和grpc-gateway

通常情况(http方法非PATCH)下,grpc-gateway会将FieldMask当作普通字段处理,意味着调用方在发送http请求时,需要显示指定FieldMask字段,grpc-gateway并不会自动填充。测试命令如下:

  • 不指定FieldMask
curl -kv http://localhost:8081/v1/fieldmasktestwithbodystar -H 'content-type: applicationo/json' -d '{"msg": "request", "nested": {"attr1": "1", "attr2": "2"}}' -X POST
{"code":200,"data":{"msg":"","nested":{"attr1":"","attr2":""}},"error":""}
  • 显式指定FieldMask
curl -kv http://localhost:8081/v1/fieldmasktestwithbodystar -H 'content-type: applicationo/json' -d '{"msg": "request", "nested": {"attr1": "1", "attr2": "2"},"field_mask": {"paths": ["msg", "nested.attr1"]}}' -X POST
{"code":200,"data":{"msg":"reply","nested":{"attr1":"attr1","attr2":""}},"error":""}

当http调用方法为PATCH,且满足以下两个条件时,grpc-gateway会自动填充FieldMask字段:

  1. PATCH路由定义在additional_bindings

  2. http body定义不为*

grpc-gateway官方描述如下:

If a binding is mapped to patch and the request message has exactly one FieldMask message in it, additional code is rendered for the gateway handler that will populate the FieldMask based on the request body. FieldMask is treated as a regular field by the gateway if the request method is not PATCH, or if the HttpRule body is "*"

测试命令如下:

curl -kv http://localhost:8081/v1/fieldmasktest -H 'content-type: applicationo/json' -d '{"attr1": "1", "attr2": "2"}' -X PATCH
{"code":200,"data":{"msg":"","nested":{"attr1":"attr1","attr2":"attr2"}},"error":""}

关于所有测试代码,可参考grpc-helloworld repo

参考连接

使用FieldMask提高C# gRpc服务性能

Patch feature

Practical API Design at Netflix, Part 1: Using Protobuf FieldMask

Netflix实用API设计第1部分:使用Protobuf FieldMask