写点什么

写给 go 开发者的 gRPC 教程 - 错误处理

  • 2023-03-04
    北京
  • 本文字数:4684 字

    阅读完需:约 15 分钟

写给 go 开发者的 gRPC 教程 - 错误处理

本篇为【写给 go 开发者的 gRPC 教程】系列第四篇


第一篇:protobuf基础

第二篇:通信模式

第三篇:拦截器

第四篇:错误处理 👈


本系列将持续更新,欢迎关注👏获取实时通知



基本错误处理

首先回顾下 pb 文件和生成出来的 client 与 server 端的接口


service OrderManagement {    rpc getOrder(google.protobuf.StringValue) returns (Order);}
复制代码


type OrderManagementClient interface {  GetOrder(ctx context.Context,            in *wrapperspb.StringValue, opts ...grpc.CallOption) (*Order, error)}
复制代码


type OrderManagementServer interface {  GetOrder(context.Context, *wrapperspb.StringValue) (*Order, error)  mustEmbedUnimplementedOrderManagementServer()}
复制代码


可以看到,虽然我们没有在 pb 文件中的接口定义设置error返回值,但生成出来的 go 代码是包含error返回值的


这非常符合 Go 语言的使用习惯:通常情况下我们定义多个error变量,并且在函数内返回,调用方可以使用errors.Is()或者errors.As()对函数的error进行判断


var (  ParamsErr = errors.New("params err")  BizErr    = errors.New("biz err"))
func Invoke(i bool) error { if i { return ParamsErr } else { return BizErr }}
func main() { err := Invoke(true)
if err != nil { switch { case errors.Is(err, ParamsErr): log.Println("params error") case errors.Is(err, BizErr): log.Println("biz error") } }}
复制代码


🌿 但,在 RPC 场景下,我们还能进行 error 的值判断么?


// common/errors.govar ParamsErr = errors.New("params is not valid")
复制代码


// server/main.gofunc (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {  return nil, common.ParamsErr}
复制代码


// client/main.goretrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil && errors.Is(err, common.ParamsErr) { // 不会走到这里,因为err和common.ParamsErr不相等 panic(err)}
复制代码


很明显,serverclient并不在同一个进程甚至都不在同一个台机器上,所以errors.Is()或者errors.As()是没有办法做判断的

业务错误码

那么如何做?在 http 的服务中,我们会使用错误码的方式来区分不同错误,通过判断errno来区分不同错误


{    "errno": 0,    "msg": "ok",    "data": {}}
{ "errno": 1000, "msg": "params error", "data": {}}
复制代码


类似的,我们调整下我们 pb 定义:在返回值里携带错误信息


service OrderManagement {    rpc getOrder(google.protobuf.StringValue) returns (GetOrderResp);}
message GetOrderResp{ BizErrno errno = 1; string msg = 2; Order data = 3;}
enum BizErrno { Ok = 0; ParamsErr = 1; BizErr = 2;}
message Order { string id = 1; repeated string items = 2; string description = 3; float price = 4; string destination = 5;}
复制代码


于是在服务端实现的时候,我们可以返回对应数据或者错误状态码


func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.GetOrderResp, error) {  ord, exists := orders[orderId.Value]  if exists {    return &pb.GetOrderResp{      Errno: pb.BizErrno_Ok,      Msg:   "Ok",      Data:  &ord,    }, nil  }
return &pb.GetOrderResp{ Errno: pb.BizErrno_ParamsErr, Msg: "Order does not exist", }, nil}
复制代码


在客户端可以判断返回值的错误码来区分错误,这是我们在常规 RPC 的常见做法


// Get Orderresp, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})if err != nil {  panic(err)}
if resp.Errno != pb.BizErrno_Ok { panic(resp.Msg)}
log.Print("GetOrder Response -> : ", resp.Data)
复制代码


🌿 但,这么做有什么问题么?


很明显,对于 clinet 侧来说,本身就可能遇到网络失败等错误,所以返回值(*GetOrderResp, error)包含error并不会非常突兀


但再看一眼 server 侧的实现,我们把错误枚举放在GetOrderResp中,此时返回的另一个error就变得非常尴尬了,该继续返回一个error呢,还是直接都返回nil呢?两者的功能极度重合


那有什么办法既能利用上error这个返回值,又能让client端枚举出不同错误么?一个非常直观的想法:让error里记录枚举值就可以了!


但我们都知道 Go 里的error是只有一个string的,可以携带的信息相当有限,如何传递足够多的信息呢?gRPC官方提供了google.golang.org/grpc/status的解决方案

使用 Status处理错误

gRPC 提供了google.golang.org/grpc/status来表示错误,这个结构包含了 codemessage 两个字段


🌲 code是类似于http status code的一系列错误类型的枚举,所有语言 sdk 都会内置这个枚举列表


虽然总共预定义了 16 个code,但gRPC框架并不是用到了每一个 code,有些 code 仅提供给业务逻辑使用


🌲 message就是服务端需要告知客户端的一些错误详情信息


package main
import ( "errors" "fmt" "log"
"google.golang.org/grpc/codes" "google.golang.org/grpc/status")
func Invoke() { ok := status.New(codes.OK, "ok") fmt.Println(ok)
invalidArgument := status.New(codes.InvalidArgument, "invalid args") fmt.Println(invalidArgument)}
复制代码

Status 和语言 Error 的互转

上文提到无论是serverclient返回的都是error,如果我们返回Status那肯定是不行的


Status 提供了和Error互转的方法



所以在服务端可以利用.Err()Status转换成error并返回


或者直接创建一个Statuserrorstatus.Errorf(codes.InvalidArgument, "invalid args")返回


func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {  ord, exists := orders[orderId.Value]  if exists {    return &ord, status.New(codes.OK, "ok").Err()  }
return nil, status.New(codes.InvalidArgument, "Order does not exist. order id: "+orderId.Value).Err()}
复制代码


到客户端这里我们再利用status.FromError(err)error转回Status


order, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})if err != nil {  // 转换有可能失败  st, ok := status.FromError(err)  if ok && st.Code() == codes.InvalidArgument {    log.Println(st.Code(), st.Message())  } else {    log.Println(err)  }
return}
log.Print("GetOrder Response -> : ", order)
复制代码


🌿 但,status真的够用么?


类似于 HTTP 状态码code的个数也是有限的。有个很大的问题就是 表达能力非常有限


所以我们需要一个能够额外传递业务错误信息字段的功能

Richer error model

Google 基于自身业务, 有了一套错误扩展 https://cloud.google.com/apis/design/errors#error_model


// The `Status` type defines a logical error model that is suitable for// different programming environments, including REST APIs and RPC APIs.message Status {  // A simple error code that can be easily handled by the client. The  // actual error code is defined by `google.rpc.Code`.  int32 code = 1;
// A developer-facing human-readable error message in English. It should // both explain the error and offer an actionable resolution to it. string message = 2;
// Additional error information that the client code can use to handle // the error, such as retry info or a help link. repeated google.protobuf.Any details = 3;}
复制代码


可以看到比标准错误多了一个 details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展

使用示例

由于 Golang 支持了这个扩展, 所以可以看到 Status 直接就是有 details 字段的.


所以使用 WithDetails 附加自己扩展的错误类型, 该方法会自动将我们的扩展类型转换为 Any 类型


WithDetails 返回一个新的 Status 其包含我们提供的 details


WithDetails 如果遇到错误会返回nil 和第一个错误


func InvokRPC() error {  st := status.New(codes.InvalidArgument, "invalid args")
if details, err := st.WithDetails(&pb.BizError{}); err == nil { return details.Err() }
return st.Err()}
复制代码


前面提到details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。


同时,Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载


它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷


下面是一些示例 error_details 载荷:


  • ErrorInfo 提供既稳定可扩展的结构化错误信息。

  • RetryInfo:描述客户端何时可以重试失败的请求,这些内容可能在以下方法中返回:Code.UNAVAILABLECode.ABORTED

  • QuotaFailure:描述配额检查失败的方式,这些内容可能在以下方法中返回:Code.RESOURCE_EXHAUSTED

  • BadRequest:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT

服务端

package main
import ( "fmt"
pb "github.com/liangwt/note/grpc/error_handling/error" epb "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) { ord, exists := orders[orderId.Value] if exists { return &ord, status.New(codes.OK, "ok").Err() }
st := status.New(codes.InvalidArgument, "Order does not exist. order id: "+orderId.Value)
details, err := st.WithDetails( &epb.BadRequest_FieldViolation{ Field: "ID", Description: fmt.Sprintf("Order ID received is not valid"), }, ) if err == nil { return nil, details.Err() }
return nil, st.Err()}
复制代码

客户端

// Get Orderorder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})if err != nil {  st, ok := status.FromError(err)  if !ok {    log.Println(err)    return  }
switch st.Code() { case codes.InvalidArgument: for _, d := range st.Details() { switch info := d.(type) { case *epb.BadRequest_FieldViolation: log.Printf("Request Field Invalid: %s", info) default: log.Printf("Unexpected error type: %s", info) } } default: log.Printf("Unhandled error : %s ", st.String()) }
return}
log.Print("GetOrder Response -> : ", order)
复制代码

引申问题

如何传递这个非标准的错误扩展消息呢?或许可以在下一章可以找到答案。

总结

我们先介绍了 gRPC 最基本的错误处理方式:返回error


之后我们又介绍了一种能够携带更多错误信息的方式:Status,它包含codemessagedetails等信息,通过Statuserror的互相转换,利用error来传输错误

参考




✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨

发布于: 刚刚阅读数: 6
用户头像

微信公众号:凉凉的知识库 2018-08-20 加入

大厂资深研发,专注golang、微服务、服务治理领域

评论

发布
暂无评论
写给 go 开发者的 gRPC 教程 - 错误处理_Go_凉凉的知识库_InfoQ写作社区