共计 3629 个字符,预计需要花费 10 分钟才能阅读完成。
在应用 Go 开发的后盾服务中,对于错误处理,始终以来都有多种不同的计划,本文探讨并提出一种从服务内到服务外的谬误传递、返回和回溯的残缺计划,还请读者们一起探讨。
问题提出
在后盾开发中,针对错误处理,有三个维度的问题须要解决:
- 函数外部的错误处理: 这指的是一个函数在执行过程中遇到各种谬误时的错误处理。这是一个语言级的问题
- 函数 / 模块的错误信息返回: 一个函数在操作谬误之后,要怎么将这个错误信息优雅地返回,不便调用方(也要优雅地)解决。这也是一个语言级的问题
- 服务 / 零碎的错误信息返回: 微服务 / 零碎在解决失败时,如何返回一个敌对的错误信息,仍然是须要让调用方优雅地了解和解决。这是一个服务级的问题,实用于任何语言
针对这三个维度的问题,笔者筹备写三篇文章一一阐明。首先本文就是第一篇: 函数外部的错误处理
高级语言的错误处理机制
一个面向过程的函数,在不同的处理过程中须要 handle 不同的错误信息;一个面向对象的函数,针对一个操作所返回的不同类型的谬误,有可能须要进行不同的解决。此外,在遇到谬误时,也能够应用断言的形式,疾速停止函数流程,大大提高代码的可读性。
在许多高级语言中都提供了 try ... catch
的语法,函数外部能够通过这种计划,实现一个对立的错误处理逻辑。而即使是 C
这种“中级语言”,尽管没有 try catch
,然而程序员也能够应用宏定义配合 goto LABEL
的形式,来实现某种程度上的谬误断言和解决。
Go 的谬误断言
在 Go 的状况就比拟难堪了。咱们先来看断言,咱们的目标是,仅应用一行代码就可能查看谬误并终止以后函数。因为没有 throw
、没有宏,如果要实现一行断言,有两种办法。
办法一:单行 if + return
第一种是把 if
的错误判断写在一行内,比方:
if err != nil {return err}
这种办法有值得商讨的点:
- 尽管合乎 Go 的代码标准,然而在实操中,if 语句中的花括号不换行这一点还是十分有争议的,并且笔者在理论代码中也很少见到过
- 代码不够直观,大抵浏览代码的时候,断言代码不显眼,而且在花括号中除了
return
之外也没法别的了,起因是 Go 的标准中强烈不倡议应用;
来分隔多条语句(if
条件判断除外)
因而,笔者强烈不倡议这么做。
办法二:panic + recover
第二种办法是借用 panic
函数,联合 recover
来实现,如以下代码所示:
func SomeProcess() (err error)
defer func() {if e := recover(); e != nil {err = e.(error)
}
}()
assert := func(cond bool, e error) {
if !cond {panic(e)
}
}
// ...
err = DoSomething()
assert(err == nil, fmt.Errorf("DoSomething() error: %w", err))
// ...
}
这种办法好不好呢?咱们要分状况看
首先,panic
的设计原意,是在当程序或协程遇到严重错误,齐全无奈持续运行上来的时候,才会调用(_比方段谬误、共享资源竞争谬误_)。这相当于 Linux 中 FATAL
级别的谬误日志,用这种机制,仅仅用来进行一般的错误处理(ERROR
级别),杀鸡用牛刀了。
其次,panic
调用自身,相比于一般的业务逻辑的零碎开销是比拟大的。而错误处理这种事件,可能是常态化逻辑,频繁的 panic - recover
操作,也会大大降低零碎的吞吐。
然而话虽这么说,应用 panic 来断言的计划,尽管在业务逻辑中基本上不必,但在测试场景下则是十分常见的。测试嘛,用牛刀有何不可?略微大一点的零碎开销也没啥问题。对于 Go 来说,十分热门的单元测试框架 goconvey 就是应用 panic
机制来实现单元测试中的断言,用的人都说好。
论断倡议
综上,在 Go 中,对于业务代码,笔者是不倡议采纳断言的,遇到谬误的时候倡议还是老老实实采纳这种格局:
if err := DoSomething(); err != nil {// ...}
而在单测代码中,则齐全能够大大方方地采纳相似于 goconvey
之类基于 panic 机制的断言。
Go 的 try … catch
家喻户晓,Go(以后版本 1.17)是没有 try ... catch
的,而且从官网的态度而言,短时间内也没有明确的打算。然而程序员有这个需要呀。这里也催生出了集中解决方案
defer 函数
笔者采纳的办法,是将须要返回的 err
变量在函数外部全局化,而后联合 defer
对立解决:
func SomeProcess() (err error) { // <-- 留神,err 变量必须在这里有定义
defer func() {
if err == nil {return}
// 这上面的逻辑,就当作 catch 作用了
if errors.Is(err, somepkg.ErrRecordNotExist) {err = nil // 这里是举一个例子,有可能捕捉到某些谬误,对于该函数而言不算谬误,因而 err = nil} else if errors.Like(err, somepkg.ErrConnectionClosed) {// ... // 或者是说遇到连贯断开的操作时,可能须要做一些重连操作之类的;甚至乎还能够在这里重连胜利之后,从新拉起一次申请} else {// ...}
}()
// ...
if err = DoSomething(); err != nil {return}
// ...
}
这种计划要特地留神变量作用域问题。
比方后面的 if err = DoSomething(); err != nil {
行,如果咱们将 err = ...
改为 err := ...
,那么这一行中的 err
变量和函数最后面定义的 (err error)
不是同一个变量,因而即使在此处产生了谬误,然而在 defer 函数中无奈捕捉到 err 变量了。
在 try ... catch
方面,笔者其实没有特地好的办法来模仿,即使是下面的办法也有一个很让人头疼的问题:defer 写法导致错误处理前置,而失常逻辑后置了。
命名的谬误处理函数
要解决前文提及的 defer 写法导致错误处理前置的问题,有第一种解决办法是比拟惯例的,那就是将 defer 前面的匿名函数改成一个命名函数,形象出一个专门的谬误处理函数。这个时候咱们能够将上一段函数进行这样的革新:
func SomeProcess() error {
// ...
if err = DoSomething(); err != nil {return unifiedError(err)
}
// ...
}
func unifiedError(err error) error {if errors.Is(err, somepkg.ErrRecordNotExist) {return nil // 有可能捕捉到某些谬误,对于该函数而言不算谬误,因而 err = nil} else if errors.Like(err, somepkg.ErrConnectionClosed) {return fmt.Errorf("handle XXX error: %w", err)
// ...
} else {return err}
}
这样就难受一些了,至多逻辑前置,错误处理后置。不过读者必定会发现——这不是什么语言都能够这么搞嘛?诚然,这怎么看都不像是对 try ... catch
的模仿, 但这种办法仍然很举荐,特地是错误处理代码很长的时候。
goto LABEL
实践上,咱们能够通过 goto
语句,将错误处理后置,比方:
func SomeProcess() error {
// ...
if err = DoSomething(); err != nil {goto ERR}
// ...
return nil
ERR:
// ...
}
对 C
语言比拟相熟的同学可能会感觉很亲切,因为在 Linux 内核中就有大量这种写法。这种写法呢,笔者其实说不出具体不好的中央,然而这个看起来很像 C 的写法,其实限度很多,反而比起 C 而言,须要留神的中央也更多:
- 仅限于 ANSI-C 的话,要求所有的局部变量都须要前置申明,这就防止了因为变量作用域而带来的同名变量笼罩;但 Go 须要留神这个问题。
- C 反对宏定义,配合前文能够实现断言,使得错误处理语句能够做得比拟优雅;而 Go 不反对
- Go 常常有很多匿名函数,匿名函数无奈
goto
到外层函数的标签,这也限度了goto
的应用
不过笔者倒也不是不反对应用 goto
,只是感觉在现有机制下,还是应用前两种模式比拟合乎 Go 的习惯。
下一篇文章是《如何在 Go 中优雅的解决和返回谬误(2)——函数 / 模块的错误信息返回》,笔者具体整顿了 Go 1.13 之后的 error wrapping
性能,敬请期待~~
本文章采纳 常识共享署名 - 非商业性应用 - 雷同形式共享 4.0 国内许可协定 进行许可。
本文最早公布于云 + 社区,也是 amc 的博客。
原作者:amc,欢送转载,但请注明出处。
原文题目:《如何在 Go 中优雅的解决和返回谬误(1)——函数外部的错误处理》
公布日期:2021-09-30
原文链接:https://segmentfault.com/a/1190000040762538。