惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
aimingoo的专栏
aimingoo的专栏
C
Check Point Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
N
Netflix TechBlog - Medium
罗磊的独立博客
F
Full Disclosure
I
InfoQ
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Register - Security
The Register - Security
The GitHub Blog
The GitHub Blog
U
Unit 42
Microsoft Security Blog
Microsoft Security Blog
Webroot Blog
Webroot Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
博客园 - 【当耐特】
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy International News Feed
Simon Willison's Weblog
Simon Willison's Weblog
T
Threat Research - Cisco Blogs
Y
Y Combinator Blog
P
Proofpoint News Feed
B
Blog RSS Feed
G
GRAHAM CLULEY
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cisco Talos Blog
Cisco Talos Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
I
Intezer
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
L
LangChain Blog
B
Blog
雷峰网
雷峰网
K
Kaspersky official blog
S
Secure Thoughts
Security Latest
Security Latest
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Security @ Cisco Blogs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的? Redis源码阅读:list实现(ziplist, quicklist)
Golang gRPC 错误处理
Jiajun Huang · 2022-11-04 · via Jiajun的技术笔记

gRPC 最常见的错误处理方式就是直接返回错误,例如 return nil, err,但是实际业务中,我们还有业务码需要返回,常见的方式 是在返回的结构体里定义一个错误码,但是这样写起来又很是麻烦,例如,你可能需要这样写:

user, err := dao.GetUserByEmail(ctx, email)
if err != nil  {
    if err == gorm.RecordNotFound {
        return &GetUserResp{Code: USER_NOT_FOUND, Msg: "user not found"}, nil
    }
    return nil, err
}

这里有几个问题:

  1. 返回错误写起来很麻烦,因为需要每次都判断错误,然后转换成对应的错误码写在 Code, Msg 两个字段中
  2. 如果直接返回 err,而不是 grpc 自定义的 codes.NotFound 等错误,无法在客户端中进行识别
  3. 如果使用了 gRPC Gateway,对于返回了非 grpc 自定义错误的地方,统统会表示成500

上面几个问题,分开来都有解法,例如对于1,可以直接返回err,但是会导致问题2;对于问题2,可以使用1,但是写起来麻烦;对于 问题3,可以使用grpc内置的错误,但是表达能力非常受限,无法传达业务错误码。

因此,为了解决这一系列问题,在比较多个错误处理库之后,我们整理了一整套结合他们优点同时又适配业务需求的错误处理体系。

错误处理库

Python的异常体系是一个非常值得借鉴的设计。首先我们会将程序的非正常执行分为错误和异常,在Go语言中,错误是我们希望能够 进行检查和处理的,而异常是只能通过 recover 尝试进行恢复的。

我们首先将错误,分为错误的类型,和错误的实例。定义错误时,我们定义的是错误的类型,这里就携带了它所应该展示的HTTP状态码 和业务错误码。当抛出错误时,也就是实例化错误的时候,此时携带上错误的栈信息、执行信息等。

例如,定义错误:

ErrBadRequest       = RegisterErrorType(BaseErr, http.StatusBadRequest, ErrCodeBadRequest)             // 400
ErrUnauthorized     = RegisterErrorType(BaseErr, http.StatusUnauthorized, ErrCodeUnauthorized)         // 401
ErrPaymentRequired  = RegisterErrorType(BaseErr, http.StatusPaymentRequired, ErrCodePaymentRequired)   // 402
ErrForbidden        = RegisterErrorType(BaseErr, http.StatusForbidden, ErrCodeForbidden)               // 403
ErrNotFound         = RegisterErrorType(BaseErr, http.StatusNotFound, ErrCodeNotFound)                 // 404
ErrMethodNotAllowed = RegisterErrorType(BaseErr, http.StatusMethodNotAllowed, ErrCodeMethodNotAllowed) // 405

实例化错误:

err = validateReq(req)
if err != nil {
    return nil, errs.NewBadRequest(err.Error(), err)
}

检测错误类型:

if errs.IsError(err, ErrBadRequest) {
    //
}

提取错误:

if baseErr, ok := errs.AsBaseErr(err); ok {
    //
}

有了上面这一套错误库以后,我们就可以愉快的携带错误栈信息、错误类型、错误业务码、错误HTTP状态码、错误信息、导致错误发生 的元错误在整个代码体系中流转,并且还可以进行类型判断、信息提取。那么怎么和 gRPC 结合在一起呢?

gRPC 错误处理

上面我们说过,如果直接使用 return nil, err 的形式,客户端无法准确识别,而如果使用 return Resp{Code, Msg}, nil 的形式, 写起来又很麻烦,而且 gRPC gateway 无法准确翻译成对应的 HTTP 状态码。

我们的解决方案就是,直接返回上一节描述的错误体系,例如:

func (s *service) CreateUser(ctx context.Context, req *pb.CreateUserReq) (*pb.CreateUserResp, error) {
    err = validateReq(req)
    if err != nil {
        return nil, errs.NewBadRequest(err.Error(), err)
    }
}

然后在中间件中,提取 Resp 并且将 codemsg 进行赋值:

func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        resp, err := handler(ctx, req)
        if err == nil {
            return resp, err
        }

        if errs.IsError(err, errs.BaseErr) {
            return resp, err
        }

        if val := reflect.ValueOf(resp); !val.IsValid() || val.IsNil() {
            tp := getRespType(ctx, info)
            if tp == nil {
                return resp, err
            }
            resp = reflect.New(tp).Interface()
        }

        if be, ok := errs.AsBaseErr(err); ok {
            grpc.SetHeader(ctx, metadata.Pairs("x-http-code", fmt.Sprintf("%d", be.HTTPCode())))
            return baseErrSetter(resp, be)
        }
    }
}

这样我们就可以将返回的错误自动序列化到Resp中对应的字段。

gRPC gateway 状态码

如果上一步,我们处理完了错误之后,直接返回error,由于不是gRPC体系内的错误码,gRPC gateway会返回500,但如果我们返回nil, gRPC gateway又会返回200,这两者都不符合预期。既然我们错误体系已经包含了HTTP状态码,是否可以直接使用呢?答案是是的,看上 面的代码中,最后我们设置了一个metadata x-http-code,我们可以在 gRPC gateway 中注册一个中间件,用这里传递的状态码:

mux := runtime.NewServeMux(
    runtime.WithForwardResponseOption(GRPCGatewayHTTPResponseModifier),
)

func GRPCGatewayHTTPResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
    md, ok := runtime.ServerMetadataFromContext(ctx)
    if !ok {
        return nil
    }

    // set http status code
    if vals := md.HeaderMD.Get(httpStatusCodeKey); len(vals) > 0 {
        code, err := strconv.Atoi(vals[0])
        if err != nil {
            return err
        }
        // delete the headers to not expose any grpc-metadata in http response
        delete(md.HeaderMD, httpStatusCodeKey)
        delete(w.Header(), grpcHTTPStatusCodeKey)
        w.WriteHeader(code)
    }

    return nil
}

这样,我们在 gRPC 中返回的是 ErrBadRequest 的实例,最终体现在 gRPC gateway 的响应中就会是400,返回的是 ErrForbidden, 在 gRPC gateway 中就会体现为 403,我们的目的就成功达成了。

监控

同时我们还提供了一套中间件,能够结合 sentry 收集错误栈。

总结

这一套整套体系,最终可以达到的效果是:

  • 能结合 gRPC 与 HTTP,符合对应规范,能充分支持业务需求
  • 错误有分级和分类,能组成错误树
  • 能识别和判断类型,能包含足够多的信息,能够自定义错误和错误类型
  • 能结合 sentry 和监控系统进行错误收集和监控
  • 使用简单方便,通俗易懂
  • 能够保持 grpc gateway 与 grpc 中状态码、错误码一致

ref: