Golang语言有很多优点,显式类型定义、静态编译打包、高性能的并发,协程和通道。但是也有一些令人诟病的地方,其中Golang错误处理,”五行代码,三行错误处理”。那么怎么正确的处理和对待Golang错误处理机制呢?今天虫虫就和大家一起来说说Golang的错误处理。
源起
作为现代 编程语言 的鼻祖之一,C语言从一开始就没有专门的错误或异常处理机制。码农需要明确的指出代码是否能按预期执行操作,或者返回抛出一个错误代码。
异常处理的概念最早在上世界60年代的LISP语言中,但直到上实际80年代它才被广泛采纳。C++让程序员习惯于”try…catch”捕捉异常,最后各语言都提供了类似的语法结构。
在Golang错误处理中,变量名称err遍布各处。不论Golang项目有多大和多重要,普遍的格式化错误结构如下:
f, err := os. Open (filename)
if err != nil {
// Handle the error here
}
根据Golang的约定,每个可能导致错误的函数都将error其作为最后一个返回值,码农有责任在每一步都正确处理它,所以golang代码中随处都是”if err != nil”语句。用条件处理每一个错误令人反感,而且非常不好看。
拒绝错误
“这设计有问题,为啥处处要检查错误”,估计每一个刚入Golang的人都会有这样的共识。
异常处理面临的一个问题是你永远不知道该函数是否会抛出一个。当然,Java throws在函数签名中有一个关键字,旨在使异常流程比较简洁,但是一旦预期的异常数量增加,就很难准确捕捉到具体异常所在。在Go以外的语言中,你需要将所有内容包装在相同的内容中try…catch。Golang中,将他严格规定下来:可能失败的每个函数都应该返回一个error类型作为最后一个值,并且随后对其处理。由于Golang的零值概念,你通常可以在没有错误处理的情况下忽略错误处理。
以上代码执行把字符串转为int64,由于此处ParseInt根本不可能有错误,所以我们用_忽略错误处理,而且如果转化错误的话i值为0。
但是,如果仅仅考虑异常处理,而不用他返回值,这样处理仍然可能会抛出异常。需要验证以确定error类型是否是函数签名的一部分是有意义的 。下面的语句我们不需要其他函数返回值,尽管他会返回一个error,如果我们不对其做检查,代码可以编译通过
http.ListenAndServe(“:8080”, nil)
当然最好的做法是,检查其返回错误,并处理。
err := http.ListenAndServe(“:8080”, nil)
if err != nil {
log.Fatalf(“Can’t start the server: %s”, err)
}
咬牙切齿
“大家编程语言都有”正常”的错误处理。为什么我应该使用这个奇葩垃圾的错误处理呢?
如果您曾在Rails中使用过Active Record,那么您可能熟悉这种代码:
user = User.new(user_params)
if user.save
head :ok
else
render json: user.errors, status: 422
end
调用user.save返回一个布尔值,指示的实例User是否已成功保存。一个调用user.errors返回可能发生的错误列表:errors对象显示为save调用的异常抛出,这种方法经常被批评为反模式。
然而,Go具有用于报告功能的”故障细节”的内置模式,并且它没有副作用。毕竟,error在Go中只是一个具有单一方法的接口:
type error interface {
Error() string
}
我们也可以随意扩展这个结果,让其提供详细地,我们需要地错误细节,比如我们可以定义如下类型:
type ValidationErr struct {
ErrorMessages map[string][]string
}
func (e *ValidationErr) Error() string {
return FormatErrors(e.ErrorMessages)
}
此处省略了FormatErrors()定义,因为它主题无关。我们只是说明它将错误消息组合成一个字符串。现在让我们假设我们使用一些用Golang编写类似Rails类框架。Action处理程序可能如下所示:
这样,验证错误是函数返回的合法部分,我们减少了副作用user.Save()。所有意外错误的处理都是在开放时发生的,并没有隐藏在框架的引擎盖下。如果出现问题,我们可以自由采取对应方法处理,然后再继续。
权衡
Golang中在代码中可能发生的每个地方处理错误非常麻烦。我们需要将所有错误放过,堆到可以批量处理的地方处理最常见的方法是使用嵌套函数调用,处理来自首先调用的主函数内的助手的所有错误。
看一下这个公认的函数调用函数的例子,该函数调用另一个函数。我们想要处理最顶层的所有错误:
这是一种惯用的Golang方式的错误处理,看起来又繁琐又笨重。幸运的是,该语言的社区已经意识到这个问题,也有了改进的方案,Golang 2中将会有一个全新错误处理方法(和支持范型一样)。官方错误处理草案设计引入了一个新的”check..handle”结构:
check语句适用于类型error或函数调用的表达式,返回以值类型结尾的值列表error。如果错误是非nil,check通过返回使用错误值调用处理程序链的结果闭包函数返回。
handle语句定义了一个块,我们姑且称之为处理程序,用于处理由check检测到的错误。处理程序中的return语句会使闭包函数立即返回给定的返回值。仅当闭包函数没有结果或使用命名结果时,才允许返回不带值。在后一种情况下,函数返回那些结果的当前值。
让我们看看我们方便的平方根计算的实例:
这代码看起来很不错,让我们期盼Go 能快点到来。
我们也可以,使用一些语言技巧来减少if…else语句数量,并且仍然可以让我们遇到单点故障。
defer 将函数调用推送到将在周围函数返回后执行的列表。
func Foo() {
f, _ := os.Open(“filename”)
defer f.Close()
// …
}
panic 停止普通的控制流程并开始”恐慌”。当一些函数开始”恐慌”时,它的执行停止,进程进入调用堆栈执行所有延迟函数,并且在当前goroutine的根目录下,程序崩溃。
recover 重新获得对”恐慌” goroutine 的控制并返回提供给它的接口panic。它仅在延迟函数内有用,在其他地方它将返回nil。
这个方法来源于Gin的源代码,Gin是Go world中一个流行的Web框架。在Gin中,如果在处理请求时发生严重错误,你可用panic(err)在处理程序内部调用,并且Gin将在hood下恢复,记录错误消息,并返回http 500状态码给用户。
“panic驱动错误处理”的方法很简单:panic嵌套调用返回一个error,然后在专门的的地方恢复并处理:
样式风格,看起来和try…catch还很有差异,但它仍然允许我们将单个错误处理,集中起来,在一起处理。
其他方法
我们努力排除在代码中的单点故障,但是当运行一些goroutines时,一切都崩溃了。我们上面提到基于技巧(panic)的错误处理业务完全没有意义。
不要惊慌,就算不用panic,在一个地方集中处理goroutines中出现的问题仍然是可能的:
Channels和sync.WaitGroup
可以结合使用Go的通道和内置的sync.Waitgroup,使goroutines在专用通道上报告错误,并在异步处理完成后依次处理它们:
当需要”收集”多个goroutine中发生的所有错误时,这种方式很有用的。
但实际上,我们很少需要处理每个错误。在大多数情况下,它是全有或全无。我们只需要知道goroutine是否失败。为此,我们将使用Golang官方子库之一的 errgroup包。方法如下:
只有errgroup.Group返回从内部启动的子程序中的第一个非零错误(如果有的话)。
PanicGroup
正如我们之前提到的,所有goroutines都在他们自己的层面上”恐慌”,如果你想在你的goroutines中使用基于panic驱动的错误处理 ,你必须做更多的工作。那我们就自建一个PanicGrop来处理:
现在可以对PanicGroup如下处理:
这就是我们如何在处理多个goroutine时保持代码清晰简洁的方法,每个goroutine都能够引发自己的”恐慌”。
接受之,坦然处之
本文中,虫虫带领大家了解了Golang的错误处理,吐了槽、改了进,变了通。也展望了Golang 2的新语法。最后,我们的结论应该是Golang中的错误处理不应该当做垃圾一概否决掉,而是将它作为一种灵活而强大的流量控制工具。每当你需要在错误发生后立即处理错误时 “if err != nil”是一个最好的方案。
如果你需要在一个地方集中处理所有错误,变通下。