gRPC 錯誤處理

gRPC 具有很好的錯誤處理機制。在前面的代碼中,我們返回的錯誤和 Go 標準庫中的代碼一樣。儘管這段代碼可以處理不同計算機之間調用,但由於 gRPC 抽象了網絡細節,讓你對底層細節所知不多。默認情況下,您的錯誤只有一個字符串描述,但是你可能希望錯誤能包括更多的信息,如狀態碼或一些其他任意數據。

Go 的 gRPC 實現中有一個非常實用的 status 包,你可以用來創建帶狀態碼的錯誤或者添加任何其他附加信息。使用該包中的 Error 函數可以創建帶狀態碼的錯誤實例,status 包中定義了符合錯誤類型的狀態碼。你在錯誤上附加的任何狀態代碼必須是 status 包中定義的,目的是爲了保持不同語言的 gRPC 實現中狀態碼一致。例如,在日誌中沒找到特定的記錄,你可以使用 NotFound 錯誤碼:

err := status.Error(codes.NotFound, "id was not found")
return nil, err

在客戶端,你需要使用 status 包中的 FromError 函數從錯誤信息中解析出狀態碼。你的目標是儘可能出現含狀態碼錯誤,這樣你就知道錯誤發生的原因,並能夠優雅地處理。但有時不含狀態碼的內部錯誤也是可能發生的。以下是如何使用 FromError 函數來解析 gRPC 錯誤:

st, ok := status.FromError(err)
if !ok {
	//不含狀態碼的錯誤處理
}
//使用st.Message(),和st.Code()方法分別讀取錯誤信息和錯誤碼

當你想要的不僅僅是一個狀態碼 (比如你試圖調試一個錯誤,想要更多的細節,如日誌或跟蹤信息),你可以使用 status 包的 WithDetails 函數,它允許你在錯誤中附加任何 protobuf 消息。

errdetails 包提供了實用的 protobufs,包括用於處理錯誤請求的消息、調試信息和本地化消息。使用該包中的 LocalizedMessage 更改前面的示例,以返回給用戶安全的錯誤消息作爲響應。在下面的代碼中,我們首先創建一個新的 NotFound 狀態碼錯誤,然後指定地區的本地化消息。接下來我們將詳細信息附加到狀態碼錯誤中:

st := status.New(code.NotFound, "id was not found")
d := &errdetails.LocalizedMessage{
	Locale: "en-US",
	Message: fmt.Sprintf("we couldn't find a user with the email address:  %s", id),
}
var err error
st, err = st.WithDetails(d)
if err != nil {
	panic(fmt.Sprintf("Unexpected error attaching metadata: %v", err))
}
return st.Err()

爲了在客戶端提取這些細節,你需要將錯誤轉換回狀態實例,通過它的 Details 方法提取細節,然後將細節的類型轉換爲匹配你在服務器上設置的 protobuf 的類型,在我們的例子中是 * errdetails.LocalizedMessage。代碼如下:

st := status.Convert(err)
for _, detail := range st.Details() {
    switch t := detail.(type) {
	case *errdetails.LocalizedMessage:
	//將t.Message發送給用戶
    }
}

回到我們日誌項目代碼中,定義一個 ErrOffsetOutOfRange 錯誤,當客戶端試圖使用日誌範圍外的偏移量時,服務器將給客戶端返回該錯誤。在 api/v1 目錄中創建 error.log 文件,添加以下代碼:

proglog/api/v1/error.go

package log_v1

import (
	"fmt"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/status"
)

type ErrOutOfRange struct {
	Offset uint64
}

func (e ErrOutOfRange) GRPCStatus() *status.Status {
    st := status.New(
        404,
        fmt.Sprintf("offset out of range: %d", e.Offset),
	)
    msg := fmt.Sprintf("the request offset is outside the log's range: %d", e.Offset)

    d := &errdetails.LocalizedMessage{
	    Locale:  "en-US",
	    Message: msg,
	}
	std, err := st.WithDetails(d)
	if err != nil {
	    return st
	}
    return std
}

func (e ErrOutOfRange) Error() string {
    return e.GRPCStatus().Err().Error()
}

接下來,更新日誌中的錯誤。在 internal/log/log.go 文件中找到 Read(offset uint64) 方法:

if s == nil || s.nextOffset < off {
    return nil, fmt.Error("offset out of range: "%d", off)
}

將返回錯誤更新如下:

if s == nil || s.nextOffset < off {


    return nil, api.ErrOutOfRange{Offset: off}

}

return s.Read(off)

最後,需要更新對應的測試代碼,在 internal/log/log_test.go 文件中,添加如下代碼:

func testOutOfRangeErr(t *testing.T, log *Log) {
    read, err := log.Read(1)
    require.Nil(t, read)
    apiError := err.(api.ErrOutOfRange)
    require.Equal(t, uint64(1), apiError.Offset)
}

有了上面的自定義錯誤,客戶端在讀取超出日誌範圍的記錄時,將返回包含很多細節的錯誤,包括本地化信息,狀態碼和錯誤消息。因爲自定義錯誤是一個結構體類型,可以在 Read(offset uint64) 中使用 type-swithch 判斷錯誤類型。我們在 ConsumeStream 方法中就使用到了該功能,如下所示:

func (s *grpcServer) ConsumeStream(req *api.ConsumeRequest, stream api.Log_ConsumeStreamServer) error {
    for {
	select {
	case <-stream.Context().Done():
		return nil
	default:
		res, err := s.Consume(stream.Context(), req)
		switch err.(type) {
		case nil:
		case api.ErrOutOfRange:
			continue
		default:
			return err
		}
		if err = stream.Send(res); err != nil {
			return err
		}
		    eq.Offset++
	    }
    }
}

我們已經改進了 gRPC 服務的錯誤處理,包括狀態代碼和可讀的本地化錯誤消息,以幫助用戶知道失敗發生的原因。

gRPC 的錯誤處理就介紹到這裏,後面我們會爲 Log 結構體定義一個 interface 類型字段,這樣我們就可以傳入不同的日誌實現,使服務更容易編寫單元測試。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zRX7i7N8CKKK8zVx_5Yjng