乐趣区

关于grpc:写给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.go
var ParamsErr = errors.New("params is not valid")
// server/main.go
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {return nil, common.ParamsErr}
// client/main.go
retrievedOrder, 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 Order
resp, 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 仅提供给业务逻辑应用

Code Number Description
OK 0 胜利
CANCELLED 1 调用勾销
UNKNOWN 2 未知谬误

🌲 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…

// 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 Order
order, 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 设计指南 - 谬误

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

退出移动版