扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
https://waterflow.link/articles/
创新互联主营遂川网站建设的网络公司,主营网站建设方案,成都App制作,遂川h5微信小程序搭建,遂川网站营销推广欢迎遂川等地区企业咨询
我们都知道当发起http请求的时候,服务端会返回一些http状态码,不管是成功还是失败。客户端可以根据服务端返回的状态码,判断服务器出现了哪些错误。
我们经常用到的比如下面这些:
同样的,当我们调用 gRPC 调用时,客户端会收到带有成功状态的响应或带有相应错误状态的错误。 客户端应用程序需要以能够处理所有潜在错误和错误条件的方式编写。 服务器应用程序要求您处理错误并生成具有相应状态代码的适当错误。
发生错误时,gRPC 会返回其错误状态代码之一以及可选的错误消息,该消息提供错误条件的更多详细信息。 状态对象由一个整数代码和一个字符串消息组成,这些消息对于不同语言的所有 gRPC 实现都是通用的。
gRPC 使用一组定义明确的 gRPC 特定状态代码。 这包括如下状态代码:
详细的状态code、number和解释可以参考这里:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
之前的章节中我们写过关于简单搭建grpc的文章:https://waterflow.link/articles/
我们在这个基础上稍微修改一下,看下下面的例子。
首先我们在服务端,修改下代码,在service的Hello方法中加个判断,如果客户端传过来的不是hello,我们我们将返回grpc的标准错误。像下面这样:
func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
// 返回参数不合法的错误
if args.GetValue() != "hello" {
return nil, status.Error(codes.InvalidArgument, "请求参数错误")
}
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)
// 调用Hello方法,并传入字符串hello
reply, err := client.Hello(ctx, &helloservice.String{Value: "hello"})
if err != nil {
log.Fatal(err)
}
log.Println("unaryRpc recv: ", reply.Value)
}
我们开启下服务端,并运行客户端代码:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:05:18 unaryRpc recv: hello:hello
可以看到会输出正确的结果。现在我们修改下客户端代码:
func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
// 调用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)
}
然后运行下客户端代码:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:14:13 rpc error: code = InvalidArgument desc = 请求参数错误
exit status 1
可以看到我们获取到了服务端返回的错误。
有时候客户端通过服务端返回的不同错误类型去做一些具体的处理,这个时候客户端可以这么写:
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)
}
// 判断服务端返回的是否是指定code的错误
if fromError.Code() == codes.InvalidArgument {
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}
我们可以看下status.FromError的返回结果:
GRPCStatus() *Status
,返回相应的状态。我们重新执行下客户端代码:
go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:26:11 invalid arguments
exit status 1
可以看到,当服务端返回的是codes.InvalidArgument错误时,我们重新定义了错误。
当我们服务端返回grpc错误时,我们想带上一些自定义的详细错误信息,这个时候就可以像下面这样写:
func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
if args.GetValue() != "hello" {
errorStatus := status.New(codes.InvalidArgument, "请求参数错误")
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
}
我们重点看下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 {
// 获取错误的详细信息,因为详细信息返回的是数组,所以这里我们需要遍历
for _, detail := range fromError.Details() {
detail = detail.(*proto.Message)
log.Println(detail)
}
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}
接着重启下服务端,运行下客户端代码:
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
可以看到详细信息打印出来了。
现实中我们可能会有这样的要求:
我们可以创建一个自定义测错误类:
package xerr
import (
"fmt"
)
/**
常用通用固定错误
*/
type CodeError struct {
errCode uint32
errMsg string
}
//返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {
return e.errCode
}
//返回给前端显示端错误信息
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服务端实现一个拦截器,目的是把自定义错误转换成grpc错误:
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 { //自定义错误类型
//转成grpc err
err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
}
}
return resp, err
}
然后客户端处理错误代码的部分修改如下:
//错误返回
causeErr := errors.Cause(err) // err类型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型
//自定义CodeError
errcode = e.GetErrCode()
errmsg = e.GetErrMsg()
} else {
errcode := uint32(500)
errmsg := "系统错误"
}
其中用到的errors.Cause的作用就是递归获取根错误。
这其实就是go-zero中实现自定义错误的方式,大家可以自己写下试试吧。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流