https://waterflow.link/articles/
創(chuàng)新互聯(lián)主營遂川網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,成都App制作,遂川h5微信小程序搭建,遂川網(wǎng)站營銷推廣歡迎遂川等地區(qū)企業(yè)咨詢
我們都知道當(dāng)發(fā)起http請(qǐng)求的時(shí)候,服務(wù)端會(huì)返回一些http狀態(tài)碼,不管是成功還是失敗??蛻舳丝梢愿鶕?jù)服務(wù)端返回的狀態(tài)碼,判斷服務(wù)器出現(xiàn)了哪些錯(cuò)誤。
我們經(jīng)常用到的比如下面這些:
同樣的,當(dāng)我們調(diào)用 gRPC 調(diào)用時(shí),客戶端會(huì)收到帶有成功狀態(tài)的響應(yīng)或帶有相應(yīng)錯(cuò)誤狀態(tài)的錯(cuò)誤。 客戶端應(yīng)用程序需要以能夠處理所有潛在錯(cuò)誤和錯(cuò)誤條件的方式編寫。 服務(wù)器應(yīng)用程序要求您處理錯(cuò)誤并生成具有相應(yīng)狀態(tài)代碼的適當(dāng)錯(cuò)誤。
發(fā)生錯(cuò)誤時(shí),gRPC 會(huì)返回其錯(cuò)誤狀態(tài)代碼之一以及可選的錯(cuò)誤消息,該消息提供錯(cuò)誤條件的更多詳細(xì)信息。 狀態(tài)對(duì)象由一個(gè)整數(shù)代碼和一個(gè)字符串消息組成,這些消息對(duì)于不同語言的所有 gRPC 實(shí)現(xiàn)都是通用的。
gRPC 使用一組定義明確的 gRPC 特定狀態(tài)代碼。 這包括如下狀態(tài)代碼:
詳細(xì)的狀態(tài)code、number和解釋可以參考這里:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
之前的章節(jié)中我們寫過關(guān)于簡(jiǎn)單搭建grpc的文章:https://waterflow.link/articles/
我們?cè)谶@個(gè)基礎(chǔ)上稍微修改一下,看下下面的例子。
首先我們?cè)诜?wù)端,修改下代碼,在service的Hello方法中加個(gè)判斷,如果客戶端傳過來的不是hello,我們我們將返回grpc的標(biāo)準(zhǔn)錯(cuò)誤。像下面這樣:
func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
// 返回參數(shù)不合法的錯(cuò)誤
if args.GetValue() != "hello" {
return nil, status.Error(codes.InvalidArgument, "請(qǐng)求參數(shù)錯(cuò)誤")
}
reply := &String{Value: "hello:" + args.GetValue()}
return reply, nil
}
我們客戶端的代碼像下面這樣:
func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
// 調(diào)用Hello方法,并傳入字符串hello
reply, err := client.Hello(ctx, &helloservice.String{Value: "hello"})
if err != nil {
log.Fatal(err)
}
log.Println("unaryRpc recv: ", reply.Value)
}
我們開啟下服務(wù)端,并運(yùn)行客戶端代碼:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:05:18 unaryRpc recv: hello:hello
可以看到會(huì)輸出正確的結(jié)果?,F(xiàn)在我們修改下客戶端代碼:
func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
// 調(diào)用Hello方法,并傳入字符串f**k
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
log.Fatal(err)
}
log.Println("unaryRpc recv: ", reply.Value)
}
然后運(yùn)行下客戶端代碼:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:14:13 rpc error: code = InvalidArgument desc = 請(qǐng)求參數(shù)錯(cuò)誤
exit status 1
可以看到我們獲取到了服務(wù)端返回的錯(cuò)誤。
有時(shí)候客戶端通過服務(wù)端返回的不同錯(cuò)誤類型去做一些具體的處理,這個(gè)時(shí)候客戶端可以這么寫:
func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
fromError, ok := status.FromError(err)
if !ok {
log.Fatal(err)
}
// 判斷服務(wù)端返回的是否是指定code的錯(cuò)誤
if fromError.Code() == codes.InvalidArgument {
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}
我們可以看下status.FromError的返回結(jié)果:
GRPCStatus() *Status
,返回相應(yīng)的狀態(tài)。我們重新執(zhí)行下客戶端代碼:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:26:11 invalid arguments
exit status 1
可以看到,當(dāng)服務(wù)端返回的是codes.InvalidArgument錯(cuò)誤時(shí),我們重新定義了錯(cuò)誤。
當(dāng)我們服務(wù)端返回grpc錯(cuò)誤時(shí),我們想帶上一些自定義的詳細(xì)錯(cuò)誤信息,這個(gè)時(shí)候就可以像下面這樣寫:
func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
if args.GetValue() != "hello" {
errorStatus := status.New(codes.InvalidArgument, "請(qǐng)求參數(shù)錯(cuò)誤")
details, err := errorStatus.WithDetails(&errdetails.BadRequest_FieldViolation{
Field: "string.value",
Description: fmt.Sprintf("expect hello, get %s", args.GetValue()),
})
if err != nil {
return nil, errorStatus.Err()
}
return nil, details.Err()
}
reply := &String{Value: "hello:" + args.GetValue()}
return reply, nil
}
我們重點(diǎn)看下WithDetails方法:
然后我們修改下客戶端代碼:
func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
fromError, ok := status.FromError(err)
if !ok {
log.Fatal(err)
}
if fromError.Code() == codes.InvalidArgument {
// 獲取錯(cuò)誤的詳細(xì)信息,因?yàn)樵敿?xì)信息返回的是數(shù)組,所以這里我們需要遍歷
for _, detail := range fromError.Details() {
detail = detail.(*proto.Message)
log.Println(detail)
}
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}
接著重啟下服務(wù)端,運(yùn)行下客戶端代碼:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:58:51 field:"string.value" description:"expect hello, get f**k"
2022/10/16 23:58:51 invalid arguments
exit status 1
可以看到詳細(xì)信息打印出來了。
現(xiàn)實(shí)中我們可能會(huì)有這樣的要求:
我們可以創(chuàng)建一個(gè)自定義測(cè)錯(cuò)誤類:
package xerr
import (
"fmt"
)
/**
常用通用固定錯(cuò)誤
*/
type CodeError struct {
errCode uint32
errMsg string
}
//返回給前端的錯(cuò)誤碼
func (e *CodeError) GetErrCode() uint32 {
return e.errCode
}
//返回給前端顯示端錯(cuò)誤信息
func (e *CodeError) GetErrMsg() string {
return e.errMsg
}
func (e *CodeError) Error() string {
return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}
然后grpc服務(wù)端實(shí)現(xiàn)一個(gè)攔截器,目的是把自定義錯(cuò)誤轉(zhuǎn)換成grpc錯(cuò)誤:
func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
resp, err = handler(ctx, req)
if err != nil {
causeErr := errors.Cause(err) // err類型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定義錯(cuò)誤類型
//轉(zhuǎn)成grpc err
err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
}
}
return resp, err
}
然后客戶端處理錯(cuò)誤代碼的部分修改如下:
//錯(cuò)誤返回
causeErr := errors.Cause(err) // err類型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定義錯(cuò)誤類型
//自定義CodeError
errcode = e.GetErrCode()
errmsg = e.GetErrMsg()
} else {
errcode := uint32(500)
errmsg := "系統(tǒng)錯(cuò)誤"
}
其中用到的errors.Cause的作用就是遞歸獲取根錯(cuò)誤。
這其實(shí)就是go-zero中實(shí)現(xiàn)自定義錯(cuò)誤的方式,大家可以自己寫下試試吧。