为什么围棋的错误处理是令人敬畏的

2020-07-07 20:56:11

Go臭名昭著的错误处理引起了外界对该编程语言的相当关注,该语言经常被吹捧为该语言最有问题的设计决策之一。如果您查看任何用Go编写的关于Github的项目,几乎可以保证您会比代码库中的任何其他代码更频繁地看到这些行:

尽管对于那些刚接触这门语言的人来说,这似乎是多余和没有必要的,但围棋错误被视为一等公民(价值观)的原因在编程语言理论中有着根深蒂固的历史,也是围棋作为一门语言本身的主要目标。为了改变或改进围棋处理错误的方式,人们做出了很多努力,但到目前为止,有一项建议是最成功的:

Go关于错误处理的哲学迫使开发人员将错误合并为他们编写的大多数函数的一等公民。即使您使用以下命令忽略错误,也是如此:

大多数LINTER或IDE都会发现您忽略了一个错误,并且您的团队成员在代码审查过程中肯定会看到这个错误。但是,在其他语言中,可能不清楚您的代码是否没有处理try catch代码块中的潜在异常,从而完全不透明地处理您的控制流。

没有意外的未捕获的异常日志炸毁您的终端(除了通过死机导致的实际程序崩溃)。

完全控制代码中的错误,将其作为您可以处理、返回和执行任何操作的值。

函数f()(value,error)的语法不仅容易教给新手,而且是任何围棋项目中确保一致性的标准。

需要注意的是,Go的错误语法并不强制您处理程序可能抛出的每个错误。GO简单地提供了一种模式,以确保您认为错误对程序流至关重要,但没有太多其他的东西。在程序结束时,如果出现错误,并且您使用err!=nil找到它,并且您的应用程序对此没有采取任何可行的措施,那么无论哪种方式,您都会遇到麻烦-GO不能拯救您。我们来看一个例子:

如果err:=Critical alDatabaseOperation();err!=nil{log.。如果err:=saveUser(User);err!=nil{log.。错误(";无法保存用户:%v";,错误)}。

如果出现错误,并且在调用Critical alDatabaseOperation()时出现错误!=nil,除了记录错误之外,我们不会对错误执行任何操作!我们可能会遇到数据损坏或无法智能处理的意外问题,无论是通过重试函数调用、取消进一步的程序流,还是在最坏的情况下,关闭程序。围棋并不神奇,也不能把你从这些情况中拯救出来。Go只提供了返回和使用错误作为值的标准方法,但是您仍然需要自己找出如何处理错误。

在类似Javascript Node.js运行时的环境中,您可以按如下方式构建程序,称为引发异常:

如果这些函数中的任何一个出现错误,则错误的堆栈跟踪将在运行时弹出并记录到控制台,但没有显式的、程序化的错误处理。

您的Critical alOperation函数不需要显式处理错误流,因为Try块中发生的任何异常都将在运行时引发,同时还会引发出错的堆栈跟踪。基于异常的语言的一个好处是,与Go相比,即使未处理的异常也会在运行时通过堆栈跟踪引发。在GO中,可能根本不处理关键错误,这可能会更糟糕。Go为您提供了对错误处理的完全控制,但也提供了全部责任。

对所有返回(value,error)的函数使用简单的if err!=nil代码段有助于确保首先考虑程序中的故障。您不需要纠结于复杂的嵌套try catch块,这些块可以适当地处理引发的所有可能的异常。

然而,对于基于异常的代码,您必须了解代码在没有实际处理异常的情况下可能出现的每种情况,因为它们将被try catch块捕获。也就是说,它鼓励程序员永远不要检查错误,因为他们知道,如果某些异常发生,至少会在运行时自动处理。

此代码不会确保正确处理异常。也许让上面的代码意识到异常之间的区别在于切换saveToDB(Item)和item.Text=';价格更改的顺序,这是不透明的,很难推理,并且可能会助长一些懒惰的编程习惯。在函数式编程术语中,这被称为花哨的术语:违反引用透明性。这篇来自微软2005年工程博客的博文至今仍然适用,即:

我的观点不是说异常不好,我的观点是异常太难了,我还不够聪明去处理它们。

如果err!=nil,则该模式的超强功能在于它允许简单的错误链遍历程序的层次结构,直到需要处理它们的位置。例如,由程序的主函数处理的常见GO错误可能如下所示:

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

上面的错误是(A)清楚的,(B)可操作的,(C)对于应用程序的哪些层出错有足够的上下文。我们可以将人类可读的上下文添加到这些因素中,而不是使用不可读的、隐蔽的堆栈跟踪来处理此类错误,并且应该通过如上所述的清晰错误链来处理这些错误。

此外,这种类型的错误链作为标准围棋程序结构的一部分自然出现,很可能如下所示:

//在控制器/user.go中,如果err:=database。CreateUser();err!=nil{log.。Errorf(";无法创建用户:%v";,err)}//在数据库/user.go func CreateUser()错误{if err:=db。SQLQuery(UserExistsQuery);err!=nil{return fmt。Errorf(";无法检查数据库中是否已存在用户:%v";,err)}.}//在数据库/sql.go函数SQLQuery()错误{if err:=sql。已连接();err!=nil{返回fmt。错误(";无法建立数据库连接:%v";,err)}.}//在SQL/sql.go函数已连接()错误{if noInternet{返回错误。新建(";没有互联网连接";)}.}。

上面代码的美妙之处在于,这些错误中的每一个都完全由其各自的功能命名,具有信息性,并且只处理它们所知道的责任。这种使用fmt.Errorf(";Something Worge Error:%v";,err)的错误链接使得构建可怕的错误消息变得微不足道,这些消息可以根据您对错误的定义准确地告诉您哪里出了问题。

最重要的是,如果您还想将堆栈跟踪附加到您的函数中,您可以利用非常棒的github.com/pkg/error库,它为您提供了如下功能:

它会打印出堆栈跟踪以及您通过代码创建的人类可读的错误链。如果我能总结一下我收到的关于在Go中编写惯用错误处理的最重要的建议:

对返回的错误做点什么,不要只是将它们冒泡到main,记录它们,然后忘记它们。

当我编写GO代码时,错误处理是我从不担心的一件事,因为错误本身是我编写的每个函数的中心方面,让我完全控制如何安全、以可读的方式和负责任地处理它们。

“如果…。;err!=nil“如果你写go,你可能会打字。我不认为这是好事也不是坏事。它完成了工作,很容易理解,并且它使程序员能够在程序失败时做正确的事情。剩下的就看你的了。