本篇為【寫給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)
}
很明顯,server和client並不在同一個進程甚至都不在同一個台機器上,所以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來表示錯誤,這個結構包含了 code 和 message 兩個字段
🌲 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 的互轉
上文提到無論是server和client返回的都是error,如果我們返回Status那肯定是不行的
但 Status 提供了和Error互轉的方法
所以在服務端可以利用.Err()把Status轉換成error並返回
或者直接創建一個Status的error:status.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.UNAVAILABLE或Code.ABORTEDQuotaFailure:描述配額檢查失敗的方式,這些內容可能在以下方法中返回:Code.RESOURCE_EXHAUSTEDBadRequest:描述客户端請求中的違規行為,這些內容可能在以下方法中返回: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,它包含code、message、details等信息,通過Status與error的互相轉換,利用error來傳輸錯誤
參考
- gRPC 擴展錯誤處理
- google API 設計指南-錯誤
✨ 微信公眾號【涼涼的知識庫】同步更新,歡迎關注獲取最新最有用的後端知識 ✨