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