本篇为【写给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仅提供给业务逻辑应用

CodeNumberDescription
OK0胜利
CANCELLED1调用勾销
UNKNOWN2未知谬误
.........

message就是服务端须要告知客户端的一些谬误详情信息

package mainimport (    "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...

// 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 mainimport (    "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来传输谬误

参考

  • gRPC 扩大错误处理
  • google API 设计指南-谬误

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