寫給go開發者的gRPC教程-錯誤處理

liangwt發表於2023-02-14

本篇為【寫給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僅提供給業務邏輯使用

CodeNumberDescription
OK0成功
CANCELLED1呼叫取消
UNKNOWN2未知錯誤
.........

? 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來傳輸錯誤

參考


✨ 微信公眾號【涼涼的知識庫】同步更新,歡迎關注獲取最新最有用的後端知識 ✨

相關文章