gRPC中的错误处理(Golang)

9/14/2022

gRPC 一般不在 message 中定义错误。

毕竟每个 gRPC 服务本身就带一个 error 的返回值,这是用来传输错误的专用通道。

gRPC 中所有的错误返回都应该是 nil 或者 由 status.Status 产生的一个error。这样error可以直接被调用方Client识别。

# 一、常规用法

当遇到一个go错误的时候,直接返回是无法被下游client识别的。

恰当的做法是:

  • 调用 status.New 方法,并传入一个适当的错误码,生成一个 status.Status 对象
  • 调用该 status.Err 方法生成一个能被调用方识别的error,然后返回
st := status.New(codes.NotFound, "some description")
err := st.Err()

传入的错误码是 codes.Code 类型。

此外还有更便捷的办法:使用 status.Error。它避免了手动转换的操作。

err := status.Error(codes.NotFound, "some description")

# 二、进阶用法

上面的错误有个问题,就是 code.Code 定义的错误码只有固定的几种,无法详尽地表达业务中遇到的错误场景

gRPC 提供了在错误中补充信息的机制:status.WithDetails 方法

Client 通过将 error 重新转换位 status.Status ,就可以通过 status.Details 方法直接获取其中的内容。

status.Detials 返回的是个slice, 是interface{}的slice,然而go已经自动做了类型转换,可以通过断言直接使用。

# 服务端示例

  • 生成一个 status.Status 对象
  • 填充错误的补充信息
// 生成一个 status.Status 
st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
// 填充错误的补充信息 WithDetails
ds, err := st.WithDetails(
    &epb.QuotaFailure{
        Violations: []*epb.QuotaFailure_Violation{{
            Subject:     fmt.Sprintf("name:%s", in.Name),
            Description: "Limit one greeting per person",
        }},
    },
)
if err != nil {
    return nil, st.Err()
}
return nil, ds.Err()

# 客户端的示例

  • 调用RPC错误后,解析错误信息
  • 通过断言直接获取错误详情
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
// 调用 RPC 如果遇到错误就对错误处理
if err != nil {
    // 转换错误
    s := status.Convert(err)
    // 解析错误信息
    for _, d := range s.Details() {
        // 通过断言直接使用
        switch info := d.(type) {
            case *epb.QuotaFailure:
            log.Printf("Quota failure: %s", info)
            default:
            log.Printf("Unexpected type: %s", info)
        }
    }
}

# 三、原理

这个错误是如何传递给调用方Client的呢?

是放到 metadata中的,而metadata是放到HTTP的header中的。

metadata是key:value格式的数据。错误的传递中,key是个固定值:grpc-status-details-bin。

而value,是被proto编码过的,是二进制安全的。

目前大多数语言都实现了这个机制。

# 注意

gRPC对响应头做了限制,上限为8K,所以错误不能太大。