注:这是Russ Cox解释Go错误和异常机制的一篇文章[1],网上已经有翻译文章[2],但是我认为有一些错误和不够准确的地方,所以自己花了一些时间又翻译了一遍。
我收到了许多邮件,这些邮件提到最近一篇名为“为什么我不会放弃Python投向Go”[1]的博客文章。在这篇文章里,作者说Go除了“用返回值来进行错误处理”这点不好,其他都非常不错。我想写一些东西谈谈Go为什么会用返回值来进行错误处理,可能会对大家有所帮助。
Go语言的规则是函数返回错误,这些函数不会panic[2]。如果一个文件找不到,os.Open返回一个错误,它不会panic;如果你向一个中断的网络连接写数据,net.Conn系列类型[3]的Write函数返回一个错误,它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败,因为API的设计者已经用返回的错误清楚地表明了这一点。
另一方面,也有一些操作几乎不可能失败,而且在那种境地下没有办法通知错误,也无法继续执行,这样的情况就适用panic。一个典型的例子是如果一个程序计算x[j],但是j已经越界了,这部分代码就会导致panic。像这样一个不可预期的panic是程序中一个严重的bug,在默认情况下它会杀死进程。不幸的是,这会使得很难写出健壮、防错的服务器程序,例如需要能够处理偶尔有一些错误的HTTP请求,同时保证服务器的其他部分继续运行。为了解决这样的问题,我们引入了recover,它允许一个goroutine能从发生在几个函数调用帧之下的panic中恢复执行。但是,panic会导致至少丢失一个函数调用帧[4]。我们是特意这么设计的。引用邮件原文:“这个提案与通常作为流程控制的异常模型不同,但这是一个谨慎的决定。我们不想鼓励程序员像在java语言那样混淆错误和异常”。
我提到的那篇博客在文章开始问道“为什么越界的数组会导致panic而错误的格式化字符串或者中断的网络连接不会?”答案是没有一种带内的方法在计算x[j]时报告数组越界的错误,但是有带内[6]的方法来报告错误的格式化字符串或者中断的网络连接的错误[7]。(关于格式化字符串错误处理的设计很有意思,但它和这里的讨论没有关系)
规则很简单:如果你的函数无论如何有可能失败,它就应该返回一个错误。当我调用其他package的函数时,如果这个函数实现的很好,我不需要担心它会panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它。
有件事你要意识到:Go是为编写大型软件而设计的。我们喜欢让程序保持简洁,而不是为大量程序员编写的大型程序不断投入维护成本。基于异常的错误处理的一个诱惑是,对于小的程序它工作得很好。但是在一个庞大的代码库里,对于每一行代码,每一个普通的操作,都需要考虑它们是否会触发一个需要处理的异常。这对于生产效率和工程时间是个很大的拖累。我自己在编写大型Python程序时就遇到这样的问题。必须承认,Go语言里函数返回错误对于调用者来说并不方便,但它们很明白地表明了程序和类型系统里发生错误的可能性。当遇到函数返回错误时,小程序可能只想打印出错误然后退出程序;但是更精心设计的程序通常会根据不同的错误来源作出不同的处理。在这种情况,try和catch的处理方式会比显式的错误返回值处理更冗长。虽然Python语言的10行代码用Go语言来实现确实可能会更冗长,但是,Go语言的首要目标不是编写10行代码的程序。
关于用异常来进行错误处理的陷阱的文章中,Raymond Chen的文章是我见过的最好的:
http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
一言蔽之:Go语言的开发者[8]认为,错误类型是如此重要以至于我们把它作为一种内置类型。
附注:偶尔你会看到panic和recover用于作为一些函数间的goto,像C语言里的longjmp和setjmp那样。这也挺好,但这应该只用在你自己package里,如果你的package的调用者需要知道这样的行为,那你就是错误地使用了panic和recover。
[1]http://uberpython.wordpress.com/2012/09/23/why-im-not-leaving-python-for-go/
[2]译注:panic这里指go语言的一种异常处理机制,用来表明出现严重的程序错误。
[3]译注:原文为net.Conn's,指net.UDPConn,net.TCPConn, net.UnixConn等一些列类型
[4]译注:在发生panic的函数中,余下的代码不会被执行到,执行流程直接跳出当前函数。
[5]译注:原文为in-band
[6]译注:“带内”的原文是in-band,一种通讯术语。如果可以从同一个渠道获得信息,就是带内(in-band);如果需要从另外一个渠道获得信息就是带外(out-of-band)。这里Russ Cox借用来指获得信息的方式,因为x[j]如果出错,在语法语句上没有办法设置一个变量或者其他语句来直接获得错误信息,因此说没有带内的方法;而对函数而言,数据和出错信息都可以从返回值获得,因此是带内的方法。
[7]译注:译者认为这里的意思是没有途径在计算x[j]时获取错误信息,而处理错误的格式化字符串或者中断的网络连接时可以通过函数返回值获得错误信息。
[8]译注:原文为Go developers,应为实现go语言的人,而不是使用go语言的人。
有疑问加站长微信联系(非本文作者)