Go臭名昭著的错误处理引起了外部人士对编程语言的广泛关注,该语言经常被吹捧为该语言最可疑的设计决策之一。如果您查看用Go编写的Github上的任何项目,那几乎可以保证,您将比代码库中的其他内容更频繁地看到这些行:

if err != nil {
return err
}
尽管对于语言新手来说似乎是多余的和不必要的,但Go语言中的错误被视为一等公民(值)的原因在编程语言理论中有着根深蒂固的历史,而Go语言本身就是其主要目标。已经做出了许多努力来更改或改进Go处理错误的方式,但是到目前为止,有一项提议比其他所有提议都胜出:
-如果err!= nil单独离开!

Go的错误哲学
Go关于错误处理的理念迫使开发人员将错误纳入他们编写的大多数功能的一流公民。即使您使用类似以下内容忽略错误:

func getUserFromDB() (*User, error) { … }

func main() {
user, _ := getUserFromDB()
}
大多数Linter或IDE都会发现您忽略了一个错误,并且在代码审阅过程中肯定对您的队友可见。但是,在其他语言中,可能不清楚您的代码没有在try catch代码块中处理潜在的异常,对于处理控制流是完全不透明的。

如果您以标准方式处理Go中的错误,您将获得以下好处:

没有隐藏的控制流
没有意外的uncaught exception日志炸毁您的终端(除了由于恐慌导致的实际程序崩溃)
可以完全控制代码中的错误,作为可以处理,返回和执行任何操作的值
func f() (value, error)对于新手来说,不仅语法易于学习,而且在任何Go项目中都确保其一致性的标准。

请务必注意,Go的错误语法不会强迫您处理程序可能抛出的每个错误。Go只是提供了一种模式,以确保您认为错误对于程序流至关重要,而其他方面则不多。在程序结束时,如果发生错误,并且您使用来找到它err != nil,而您的应用程序对此无能为力,那么您都将遇到麻烦-Go不能保存您。让我们看一个例子:

if err := criticalDatabaseOperation(); err != nil {
// Only logging the error without returning it to stop control flow (bad!)
log.Printf("Something went wrong in the DB: %v", err)
// WE SHOULD

return
beneath this line!
}

if err := saveUser(user); err != nil {
return fmt.Errorf("Could not save user: %w", err)
}
如果出现错误并err != nil正在调用criticalDatabaseOperation(),除了记录错误外,我们不会对错误进行任何处理!我们可能会遇到数据损坏或无法智能处理的其他无法预料的问题,方法是重试函数调用,取消进一步的程序流,或者在最坏的情况下关闭程序。Go并不是神奇的事物,也无法从这些情况中解救您。Go仅提供了一种返回并使用错误作为值的标准方法,但是您仍然必须自己弄清楚如何处理错误。

其他语言如何做到:抛出异常
在类似Javascript Node.js运行时的环境中,您可以按以下方式构建程序,称为throwing exceptions:

try {
criticalOperation1();
criticalOperation2();
criticalOperation3();
} catch (e) {
console.error(e);
}
如果这些函数中的任何一个发生错误,则错误的堆栈跟踪将在运行时弹出并记录到控制台,但不会对发生的问题进行明确的编程处理。

您的criticalOperation函数不需要显式处理错误流,因为在try块中发生的任何异常都将在运行时引发,并给出错误原因的堆栈跟踪信息。与Go相比,基于异常的语言的一个好处是,即使发生未处理的异常,即使发生,仍会在运行时通过堆栈跟踪引发该异常。在Go中,可能根本不处理严重错误,这可能会更糟。Go为您提供了对错误处理的完全控制权,还提供了全部责任。

编辑:异常绝对不是其他语言处理错误的唯一方法。例如,Rust很好地折衷了使用选项类型和模式匹配来查找错误条件,并利用一些不错的语法糖来达到类似的结果。

为什么Go不使用异常进行错误处理
围棋禅
Go的禅宗提到了两个重要的谚语:

简单性很重要
计划失败而不是成功
对if err != nil返回的所有函数使用简单的代码片段(value, error)有助于确保程序的失败是最重要的。您无需费心处理复杂的嵌套try catch块,它们可以适当地处理所有可能出现的异常。

基于异常的代码通常是不透明的
但是,使用基于异常的代码,您将不得不意识到在没有实际处理它们的情况下代码可能具有异常的每种情况,因为它们将被您的try catch块捕获。也就是说,它鼓励程序员从不检查错误,至少知道,某些异常(如果发生)将在运行时自动处理。

用基于异常的编程语言编写的函数通常看起来像这样:

item = getFromDB()
item.Value = 400
saveToDB(item)
item.Text = 'price changed'
此代码不执行任何操作以确保正确处理异常。也许使上述察觉异常的代码之间的差别是切换的顺序saveToDB(item)和item.Text = 'price changed,这是不透明的,难的原因有关,并能鼓励一些懒散的编程习惯。在函数式编程术语中,这就是花哨的术语:违反参照透明性。微软2005年的工程博客中的这篇博客文章至今仍然适用,即:

我的意思不是说异常是不好的。我的观点是,异常太难了,我不够聪明,无法处理它们。

Go的错误语法的好处
轻松创建可行的错误链
模式的超级优势在于if err != nil它如何允许简单的错误链遍历程序的层次结构,一直到需要处理的地方。例如,程序main功能处理的常见Go错误可能如下所示:

[2020-07-05-9:00]错误:无法创建用户:无法检查用户是否已存在于数据库中:无法建立数据库连接:没有互联网

上面的错误是(a)清楚的,(b)可操作的,(c)对于应用程序的哪一层出错了具有足够的了解。像这样的错误不是由难以理解的,难以理解的堆栈跟踪引起的,而是由于我们可以添加人类可读上下文的因素导致的,并且应通过如上所示的清晰错误链进行处理。

而且,这种类型的错误链自然会作为标准Go程序结构的一部分而出现,可能看起来像这样:

// In controllers/user.go
if err := db.CreateUser(user); err != nil {
return fmt.Errorf("could not create user: %w", err)
}

// In database/user.go
func (db *Database) CreateUser(user *User) error {
ok, err := db.DoesUserExist(user)
if err != nil {
return fmt.Errorf("could not check if user already exists in db: %w", err)
}

}

func (db *Database) DoesUserExist(user *User) error {
if err := db.Connected(); err != nil {
return fmt.Errorf("could not establish db connection: %w", err)
}

}

func (db *Database) Connected() error {
if !hasInternetConnection() {
return errors.New("no internet connection")
}

}
上面代码的美在于,这些错误中的每一个都完全由其各自的功能命名,具有参考价值,并且仅对它们所知道的负责。使用这种错误链接可以fmt.Errorf("something went wrong: %w", err)轻松地构建很棒的错误消息,这些错误消息可以根据您的定义准确地告诉您出了什么问题。

最重要的是,如果您还希望将堆栈跟踪附加到函数中,则可以利用出色的github.com/pkg/errors库,为您提供以下功能:

errors.Wrapf(err, "could not save user with email %s", email)
它将打印出堆栈跟踪以及您通过代码创建的易于理解的错误链。如果我可以总结一下我收到的有关在Go中编写惯用错误处理的最重要建议:

当您的错误可用于开发人员时添加堆栈跟踪

对您返回的错误进行处理,不要只是将它们冒充为主要错误,记录它们并忘记它们

保持您的错误链清晰无误

当我编写Go代码时,错误处理是我永远不会担心的一件事,因为错误本身是我编写的每个函数的核心方面,从而使我能够完全控制我如何安全,可读且负责任地处理它们。

“如果 …; err!= nil”是写go时可能要输入的内容。我不认为这是正面还是负面的。它可以完成工作,易于理解,并且可以使程序员在程序失败时执行正确的操作。其余的取决于您。