- 化茧成蝶:Go在FreeWheel服务化中的实践
- InfoQ中文站
- 1346字
- 2020-06-26 06:03:27
如何扩展Server端返回的Error
在gRPC的Server端返回Error时,虽然接口的返回值中声明的是标准库的Error,但gRPC内部会判断Error是否为特定的类型,如果不是,则会统一返回Code为“Unknown”的Error。因此,多数情况下我们需要用专门的方法构造一个Error,比如:
import "google.golang.org/gRPC/status" ... return status.Error(codes.Aborted, "aborted")
实际上,这个Error的类型是“google.golang.org/genproto/googleapis/rpc/status” 包里面的“Status”,定义如下:
type Status struct { Code int32 Message string Details []*google_protobuf.Any }
除了Details字段外,只有Code和Message,十分简洁,但如果我们有更复杂的业务需求,这些字段是不能满足的,因此我们需要扩展Error。
方案零:不要扩展gRPC预定义的Status Code
前面的例子中,“codes.Aborted”是gRPC预定义的一些Code之一,自然而然,我们会想到是否可以扩展这个Code列表。
我曾经尝试过扩展这个列表,在Go语言的gRPC客户端中是可以拿到自定义的Code的,但官方的开发人员在一些问答中表示不推荐这么做,而且一些其他语言的gRPC客户端中,一旦发现Code不在预定义的列表中,有可能直接替换成预定义的“Unknown”错误,甚至直接抛出异常,因此不要这么做。
方案一:在Response中添加err属性
这也是一种显而易见的方案,既然已有的Error不能满足需求,那就在Response对象中加入一个“err”属性,而它的类型是自己定义的,大概是这个样子:
type MyResponse struct { err MyErr ... }
可以满足需求,但很不优雅,同意么?这么做直接违反了gRPC的错误处理机制,甚至不符合Go语言的规范,所以也不推荐这么做。
方案二:通过Metadata传递
首先要说,这才是一个靠谱的方案,也是官方曾经推荐的方案。
先不说“曾经”是什么意思,我们来看看这个方案是怎么玩的。
gRPC的Client端和Server端之间,可以借助名为“Metadata”的数据结构来传递额外的信息,而我们自己扩展的Error信息就属于这个“额外信息”。
详细的用法可以参考在GitHub上关于“gRPC-metadata”的文档:
https://github.com/gRPC/gRPC-go/blob/master/Documentation/gRPC-metadata.md
此处附上其中的一些代码示例,以便让大家快速地获得一个直观的印象。
以从Server端向Client端传递Metadata为例,首先在Server端将数据准备好:
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) { // 创建并发送Header header := metadata.Pairs("header-key", "val") gRPC.SendHeader(ctx, header) // 创建并发送Trailer trailer := metadata.Pairs("trailer-key", "val") gRPC.SetTrailer(ctx, trailer) }
接着在Client端接收数据:
var header, trailer metadata.MD // 用来保存header和trailer的变量 r, err := client.SomeRPC( ctx, someRequest, gRPC.Header(&header), // 将会接收header gRPC.Trailer(&trailer), // 将会接收trailer ) // 按需求对header和trailer做处理
方案三:通过Status中的Details属性传递
最后出场的是我们实际采用的方案。
我在前面将扩展的Error信息称作“额外信息”,其实准确的说应该是“额外的Error信息”,因此,直觉上最自然的方式还是在Error对象内部携带这个信息。
你一定注意到了“Status”对象里的那个“Details”属性,它是一个“Any”对象的数组,字面上看似乎是“可以保存任何类型对象的数组”的意思,确实是这样。
gRPC最近刚刚添加了两个工具方法,使得“Details”属性变得非常易用:
// WithDetails返回一个新创建的Status对象,其中附加了参数details传入的 Message列表, // 如果有error发生,将返回nil和第一个遇到的error。 func (s *Status) WithDetails(details ...proto.Message) (*Status, error) // Details返回Status中Details携带的Message列表, // 如果decode某个Message时发生错误,那这个错误会被添加到结果列表中返回。 func (s *Status) Details() []interface{}
实际使用时很方便。
附加Details:
s, _ := status.New(codes.Aborted, "message").WithDetails(d1, d2) return nil, s.Err()
获取Details:
details, _ := s.Details() for _, d := range details { m := d.(YourType) // ... }
好了,以上就是我们在使用gRPC过程中遇到的两个问题和相应的思考,也许并不是最优的方案,但希望能给你带来一些提示。
Go语言作为一种快速发展变化中的语言,相应的技术生态还不是十分健全,包括gRPC和由此衍生的gRPC-Gateway等项目,仍然有不少提升空间。中国是目前Go语言应用人数最多、气氛最火热的国家,我们希望能看到越来越多的国内开发者参与到开源项目的发展中,也希望有越来越多的优秀项目出现。