0.1、索引

https://waterflow.link/articles/1665938704477

我们都知道当发起http请求的时候,服务端会返回一些http状态码,不管是成功还是失败。客户端可以根据服务端返回的状态码,判断服务器出现了哪些错误。

我们经常用到的比如下面这些:

  • 200:OK,请求成功
  • 204:NO CONTENT,此请求没有要发送的内容,但标头可能很有用。 用户代理可以用新的更新其缓存的资源头。
  • 400:Bad Request,由于被认为是客户端错误(例如,格式错误的请求语法、无效的请求消息帧或欺骗性请求路由),服务器无法或不会处理请求。
  • 404:Not Found,服务器找不到请求的资源。 在浏览器中,这意味着无法识别 URL。 在 API 中,这也可能意味着端点有效但资源本身不存在。

同样的,当我们调用 gRPC 调用时,客户端会收到带有成功状态的响应或带有相应错误状态的错误。 客户端应用程序需要以能够处理所有潜在错误和错误条件的方式编写。 服务器应用程序要求您处理错误并生成具有相应状态代码的适当错误。

发生错误时,gRPC 会返回其错误状态代码之一以及可选的错误消息,该消息提供错误条件的更多详细信息。 状态对象由一个整数代码和一个字符串消息组成,这些消息对于不同语言的所有 gRPC 实现都是通用的。

gRPC 使用一组定义明确的 gRPC 特定状态代码。 这包括如下状态代码:

  • OK:成功状态,不是错误。
  • CANCELLED:操作被取消,通常是由调用者取消的。
  • DEADLINE_EXCEEDED:截止日期在操作完成之前到期。
  • INVALID_ARGUMENT:客户端指定了无效参数。

详细的状态code、number和解释可以参考这里:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

1、grpc错误

之前的章节中我们写过关于简单搭建grpc的文章:https://waterflow.link/articles/1665674508275

我们在这个基础上稍微修改一下,看下下面的例子。

首先我们在服务端,修改下代码,在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

可以看到我们获取到了服务端返回的错误。

2、获取grpc错误类型

有时候客户端通过服务端返回的不同错误类型去做一些具体的处理,这个时候客户端可以这么写:

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的返回结果:

  • 如果 err 是由这个包产生的或者实现了方法 GRPCStatus() *Status,返回相应的状态。
  • 如果 err 为 nil,则返回带有代码的状态。OK 并且没有消息。
  • 否则,err 是与此包不兼容的错误。 在这个情况下,返回一个 Status 结构是 code.Unknown 和 err 的 Error() 消息,并且ok为false。

我们重新执行下客户端代码:

go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:26:11 invalid arguments
exit status 1

可以看到,当服务端返回的是codes.InvalidArgument错误时,我们重新定义了错误。

3、获取grpc错误更详细的信息

当我们服务端返回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方法:

  • 该方法传入一个proto.Message类型的数组,Message是一个protocol buffer的消息
  • 返回一个新Status,并将提供的详细信息消息附加到Status
  • 如果遇到任何错误,则返回 nil 和遇到的第一个错误

然后我们修改下客户端代码:

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

可以看到详细信息打印出来了。

4、定义标准错误之外的错误

现实中我们可能会有这样的要求:

  • 当grpc服务端是自定义错误时,客户端返回自定义错误
  • 当grpc服务端返回的是标准错误时,客户端返回系统错误

我们可以创建一个自定义测错误类:

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中实现自定义错误的方式,大家可以自己写下试试吧。

grpc错误处理的更多相关文章

  1. 微服务从代码到k8s部署应有尽有系列(十、错误处理)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  2. 一篇文章带你搞懂 etcd 3.5 的核心特性

    作者 唐聪,腾讯云资深工程师,极客时间专栏<etcd实战课>作者,etcd活跃贡献者,主要负责腾讯云大规模k8s/etcd平台.有状态服务容器化.在离线混部等产品研发设计工作. etcd ...

  3. 航空概论(历年资料,引之百度文库,PS:未调格式,有点乱)

    航空航天尔雅 选择题1. 已经实现了<天方夜谭>中的飞毯设想.—— A——美国2. 地球到月球大约—— C 38 万公里3. 建立了航空史上第一条定期空中路线—— B——德国4. 对于孔明 ...

  4. grpc编译错误解决

    berli@berli-VirtualBox:~/grpc$ make [MAKE]    Generating cache.mk [C]       Compiling src/core/lib/s ...

  5. 用Java开发gRPC服务的例子分析

    本文的代码例子来自:https://github.com/grpc/grpc-java  定义服务 这一步与其他语言完全一样,需要定义gRPC的服务.方法.request和response的类型. 完 ...

  6. 编写一个go gRPC的服务

    前置条件: 获取 gRPC-go 源码 $ go get google.golang.org/grpc 简单例子的源码位置: $ cd $GOPATH/src/google.golang.org/gr ...

  7. Google 高性能 RPC 框架 gRPC 1.0.0 发布(附精彩评论)

    gRPC是一个高性能.开源.通用的RPC框架,面向移动和HTTP/2设计,是由谷歌发布的首款基于Protocol Buffers的RPC框架. gRPC基于HTTP/2标准设计,带来诸如双向流.流控. ...

  8. 编译gRPC

    编译gRPC 目录 一.概述 二.编译gRPC 三.C#中使用gRPC 四.C++中使用gRPC 无论通过哪种语言调用gRPC,都必须要编译gRPC,因为生成proto访问类时,除了产生标准的数据定义 ...

  9. 初识google多语言通信框架gRPC系列(二)编译gRPC

    目录 一.概述 二.编译gRPC 三.C#中使用gRPC 四.C++中使用gRPC 无论通过哪种语言调用gRPC,都必须要编译gRPC,因为生成proto访问类时,除了产生标准的数据定义类之外,还需要 ...

随机推荐

  1. 来看看这位年轻的 eBay 小伙是如何成为 Committer

    介绍一下我自己 目前就职于eBay中国,专注于微服务中间件,分布式架构等领域,同时也是狂热的开源爱好者. 如何成为一个commiter 过去几个月,我一直持续在为 Apache DolphinSche ...

  2. java学习第五天异常机制.day14

    异常处理机制 确保程序的正常执行.这种机制称为异常处理机制 异常对象 常用方法 方法介绍 toString 返回异常类型和异常信息 getMessage 返回异常信息 printStackTrace ...

  3. mysql存储过程的创建和调用

    描述:存储过程就是具有名字的一段代码,用来完成一个特定的功能.创建的存储过程保存在数据库的数据词典中. --创建一个名为GreetWorld的存储过程,拼接两个值 CREATE PROCEDURE G ...

  4. 【java】学习路线11-四种权限修饰的测试

    package com.remoo.test;public class Learn09_Test{    private static String welcomeWord1 = "你好,p ...

  5. Android Kotlin Annotation Processer

    Annotation Processer 注解处理器(Annotation Processer)是javac内置的注解处理工具,可以在编译时处理注解,让我们自己做相应的处理.比如生成重复度很高的代码, ...

  6. Linux面试题 系统启动流程

    BIOS:基本输入输出系统,帮助我们初始化硬件 硬盘分区有两类:MBR和GPT ; MBR单块硬盘不能大于2T,主分区的数量不能超过4个:MBR方案存储在第一个扇区的前446个字节(共512字节,后面 ...

  7. KingbaseES R6 集群 recovery 参数对切换的影响

    案例说明:在KingbaseES R6集群中,主库节点出现宕机(如重启或关机),会产生主备切换,但是当主库节点系统恢复正常后,如何对原主库节点进行处理,保证集群数据的一致性和安全,可以通过对repmg ...

  8. 璞华PLM为全场景产品生命周期管理赋能,助力产品主线的企业数字化转型

    英文版的<产品生命周期管理(PLM)软件市场--增长.趋势.COVID-19影响和预测(2022 - 2027)>中对未来PLM市场概述的描述为:"产品生命周期管理(PLM)软件 ...

  9. C#,启动exe程序并传参(参数间带&符号)方法

    入参格式例如:C:\\Users\\Administrator\\Desktop\\测试\\测试\\bin\\Debug\\测试.exe type=1^&card_no=123 public ...

  10. P1829 [国家集训队]Crash的数字表格

    P1829 [国家集训队]Crash的数字表格 原题传送门 前置芝士 莫比乌斯反演 乘法逆元 数论分块 正文 //补充:以下式子中的除法均为整除 由题目可以得知,这道题让我们所求的数,用一个式子来表达 ...