Go臭名昭著的错误处理引起了编程语言外部人士的注意,它常常被吹捧为该语言最值得怀疑的设计决策之一。如果你研究一下用Go编写的Github上的任何项目,几乎可以保证你会比代码库中的任何其他项目更频繁地看到这些代码行:
if err != nil {
return err
}
尽管对于那些不熟悉Go语言的人来说,这似乎是多余的和不必要的,但Go中的错误被视为一等公民(价值观)的原因在编程语言理论和Go作为一种语言本身的主要目标中有着根深蒂固的历史。为了改进Go如何处理错误,已经做出了许多努力,但到目前为止,有一项提议胜于所有其他提案:
抛弃if err != nil!
Go的错误设计哲学
Go关于错误处理的哲学迫使开发人员将错误作为他们编写的大多数函数的第一类公民。即使您忽略了错误,使用的内容如下:
func getUserFromDB() (*User, error) { ... }
func main() {
user, _ := getUserFromDB()
}
大多数linter或IDE都会发现您忽略了一个错误,并且在代码评审期间,您的团队成员肯定可以看到它。但是,在其他语言中,可能不清楚您的代码没有处理try-catch代码块中的潜在异常,在处理控制流方面完全不透明。
如果您以标准方式使用Go的错误处理,您将获得以下好处:
-
没有隐藏的控制流 -
没有意外的uncaught exception日志炸毁您的终端(除了由于panic导致的实际程序崩溃) -
可以完全控制代码中的错误,作为可以处理,返回和执行任何操作的值 func f() (value, error)
对于新手来说,不仅语法易于学习,而且在任何Go项目中都确保其一致性的标准。
值得注意的是,Go的错误语法并不强制您处理程序可能抛出的每一个错误。Go只是提供了一个模式来确保您将错误是程序流至关重要的一部分,而没有其它的意思。在程序结束时,如果出现错误,并且您使用err!=nil
,假设你的应用程序对此error没有任何处理,无论怎样,你的代码都会有麻烦,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)
}
如果在调用criticalDatabaseOperation()出现错误,除了判断err != nil
、记录错误外,我们不会对错误进行任何处理!我们可能会遇到数据损坏或无法智能处理的其他无法预料的问题,或者重试函数调用,取消进一步的程序流,或者在最坏的情况下关闭程序。Go不是神奇的,也不能使您摆脱这些问题。Go仅提供了一种返回并使用错误作为值的标准方法,但是您仍然必须弄清楚如何自己处理错误。
其他语言处理方式:抛出异常
在类似Javascript Node.js
运行时的环境中,您可以按以下方式构建程序,称为throwing exceptions
:
try {
criticalOperation1();
criticalOperation2();
criticalOperation3();
} catch (e) {
console.error(e);
}
如果这些函数中的任何一个发生错误,则错误的堆栈跟踪将在运行时弹出并记录到控制台,但不会对发生的问题进行明确的代码逻辑处理。
您的criticalOperation函数不需要显式处理错误流,因为在try块中发生的任何异常都将在运行时引发,并给出错误原因的堆栈跟踪。与Go相比,基于异常的语言的一个优点是,即使发生未处理的异常,在运行时仍会通过堆栈跟踪引发未处理的异常。在Go中,可能根本不用处理严重错误,这可能会更糟。Go为您提供了对错误处理的完全控制,但也提供了全部责任。
另外异常绝对不是其他语言处理错误的唯一方法。例如,Rust很好地折衷了使用选项类型和模式匹配来查找错误条件,并利用一些不错的语法糖来达到类似的效果。
为什么Go不使用异常进行错误处理
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仍然执行,这是不透明的,难以解释,并可能鼓励一些懒惰的编程习惯。在函数式编程术语中,这被称为花哨术语:违反引用透明性。这篇来自2005年微软工程博客的博文至今仍然有效,即:
我的意思不是说异常不好。我的观点是,异常太难了,我不够聪明,无法处理它们。
Go error语法的好处
轻松创建可行的错误链
一个超级系统的模式,如果error!=nil是允许容易的错误链遍历程序的层次结构,一直到需要处理的地方。例如,由程序的main函数处理的常见Go错误可能如下所示:
[2020-07-05-9:00] ERROR: Could not create user: could not check if user already exists in DB: could not establish database connection: no internet
已经足够清晰应用程序有错误。这样的错误不是因为一个不可读的、神秘的堆栈跟踪而崩溃,而是由于我们可以添加人类可读上下文的因素导致的,应该通过上面所示的清晰的错误链来处理异常问题。
此外,这种错误链自然会作为标准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中编写惯用错误处理的最重要建议:
-
为您的错误添加可用于开发人员时堆栈跟踪 -
对返回的错误做点什么,不要只是把它们放到main上,记录下来,然后忘记它们 -
保持您的错误链明确
当我编写Go代码时,错误处理是我永远不会担心的一件事,因为错误本身是我编写的每个函数的核心问题,从而使我能够完全控制我如何安全、可读且负责任地处理它们。
if err!= nil
,这是您可能会输入的内容。我不认为这是正面还是负面的。它可以完成工作,易于理解,并且可以使程序员在程序失败时执行正确的操作,其余的取决于您。
文章评论