Go 语言的错误处理机制引发争议

红薯 发布于 2012/12/04 20:19
阅读 7K+
收藏 20
Go

最近,有关Go语言的错误处理机制在社区中展开了讨论,有人认为冗长重复的错误处理格式像是回到了上世纪七十年代,而Go语言的开发者给予了反驳。

Go语言的错误处理机制可以从支持函数多返回值说起:

在C语言当中常见的做法是保留一个返回值来表示错误(比如,read()返回0),或 者保留返回值来通知状态,并将传递存储结果的内存地址的指针。这容易产生了不安全的编程实践,因此在像Go语言这样有良好管理的语言中是不可行的。认识到 这一问题的影响已超出了函数结果与错误通讯的简单需求的范畴,Go的作者们在语言中内建了函数返回多个值的能力。作为例子,这个函数将返回整数除法的两个部分:

func divide(a, b int) (int, int) {
  quotient := a / b
  remainder := a % b
  return quotient, remainder
}

多返回值的出现促进了"comma-ok"的模式。有可能失败的函数可以返回第二个布尔结果来表示成功。作为替代,也可以返回一个错误对象,因此像下面这样的代码也就不见怪了:

if result, ok := moreMagic(); ok {
  /* Do something with result */
}

除此之外,Go语言还提供了Panic/Recover机制,陈皓在“Go语言简介”中有比较详细的描述:

对于不可恢复的错误,Go提供了一个内建的panic函数,它将创建一个运行时错误并使程序停止(相当暴力)。该函数接收一个任意类型(往往是字符串)作为程序死亡时要打印的东西。当编译器在函数的结尾处检查到一个panic时,就会停止进行常规的return语句检查。

下面的仅仅是一个示例。实际的库函数应避免panic。如果问题可以容忍,最好是让事情继续下去而不是终止整个程序。

var user = os.Getenv("USER")
func init() {
  if user == "" {
    panic("no value for $USER")
  }
}

当panic被调用时,它将立即停止当前函数的执行并开始逐级解开函数堆栈,同时运行所有被defer的函数。如果这种解开达到堆栈的顶端,程序就 死亡了。但是,也可以使用内建的recover函数来重新获得Go程的控制权并恢复正常的执行。 对recover的调用会通知解开堆栈并返回传递到panic的参量。由于仅在解开期间运行的代码处在被defer的函数之内,recover仅在被延期 的函数内部才是有用的。

你可以简单地理解为recover就是用来捕捉Painc的,防止程序一下子就挂掉了。

Python和Go语言的实践者Yuval Greenfield在“Why I’m not leaving Python for Go”的博文中批评了Go语言的错误处理机制。他首先引用了Go语言的设计者对错误处理机制的看法:

在Go语言中,错误处理非常重要。语言的设计和规范鼓励开发人员显式地检查错误(与其他语言抛出异常然后catch住是不同的)。这种机制某种程度上使得Go语言的代码冗长重复,但是幸运的是你可以利用一些技巧来把冗长的代码最小化。

Yuval表示这点他无法忍受,每一次函数的调用都需要if语句来判断是否出现错误,他引用了一段官方的所谓最小化代码量的错误处理示例:

if err := datastore.Get(c, key, record); err != nil {
  return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
  return &appError{err, "Can't display record", 500}
}

Yuval说,这就是在Go语言中调用函数的正确处理方式,甚至连Println的调用都要这样做。如果不这么做会怎样呢?Go语言并没有坚持要采 用这种冗长的错误机制。它也允许忽略这些函数调用错误。但是这样做很危险。在下面的例子中,如果第一个Get函数错误,那么程序继续调用第二个函数!这是 非常恐怖的事情。

func main() {
    http.Get("http://www.nuke.gov/seal_presidential_bunker")
    http.Get("http://www.nuke.gov/trigger_doomsday_device")
}

理论上,我们要求开发人员决不能忽略返回的错误。而实际上,只有在一些关键性的错误上面处理才是必要的。

关于panic/recover机制,Yuval认为也不够出色,因为连Go的标准库都不怎么用这种机制:为什么索引溢出的数组要比错误格式的字符 串或者失败的网络连接更需要panic呢?Go语言希望能够完全避免异常,但实际上不能,总有一些异常会在某处发生,让开发人员在错误出现时感到困惑。

针对Yuval的批评,Go的开发者Russ Cox做出了回应

在Go语言中,规定的方式是,函数返回错误信息。如果一个文件并不存在,op.Open函数会返回一个错误信息。如果你向你一个中断了的网络连接里 写数据,net.Conn里的Write方法会返回一个错误。这种状况在这种程序中是可以预料到的。这种操作就是容易失败,你知道程序会如何运行,因为 API的设计者通过内置了一种错误情况的结果而让这一切显得很清楚。

从另一方面讲,有些操作基本上不会出错,所处的环境根本不可能给你提示错误信息,不可能控制错误。这才是让人痛苦的地方。典型的例子;一个程序执行 x[j],j值超出数组边界,这才痛苦。像这样预料之外的麻烦在程序中是一个严重的bug,一般会弄死程序的运行。不幸的是,由于这种情况的存在,我们很 难写出健壮的,具有自我防御的服务器——例如,可以应付偶然出现的有bug的HTTP请求处理器时,不影响其他服务的启动和运行。为解决这个问题,我们引 入了恢复机制,它能让一个go例程从错误中恢复,服务余下设定的调用。然而,代价是,至少会丢失一个调用。这是特意而为之的。引用邮件中的原话:“这种设 计不同于常见的异常控制结构,这是一个认真思考后的决定。我们不希望像java语言里那样把错误和异常混为一谈。”

Russ Cox针对“为什么数组越界造成的麻烦会比错误的网址或断掉的网络引出的问题要大?”这个问题给出了自己的答案:

我们没有一种内联并行的方法来报告在执行x[j]期间产生的错误,但我们有内联并行的方法报告由错误网址或网络问题造成的错误。

使用Go语言中的错误返回模式的规则很简单:如果你的函数在某种情况下很容易出错,那它就应该返回错误。当我调用其它的程序库时,如果它是这样写的,那我不必担心那些错误的产生,除非有真正异常的状况,我根本没有想到需要处理它们。

最后,Russ Cox指出Go语言是为大型软件设计的:

我们都喜欢程序简洁清晰,但对于一个由很多程序员一起开发的大型软件,维护成本的增加很难让程序简洁。异常捕捉模式的错误处理方式的一个很有吸引力 的特点是,它非常适合小程序。但对于大型程序库,如果对于一些普通操作,你都需要考虑每行代码是否会抛出异常、是否有必要捕捉处理,这对于开发效率和程序 员的时间来说都是非常严重的拖累。我自己做开发大型Python软件时感受到了这个问题。Go语言的返回错误方式,不可否认,对于调用者不是很方便,但这 样做会让程序中可能会出错的地方显的很明显。对于小程序来说,你可能只想打印出错误,退出程序。对于一些很精密的程序,根据异常的不同,来源的不同,程序 会做出不同的反应,这很常见,这种情况中,try + catch的方式相对于错误返回模式显得冗长。当然,Python里的一个10行的代码放到Go语言里很可能会更冗长。毕竟,Go语言主要不是针对10行 规模的程序的。

作者:InfoQ/崔康 热情的技术探索者,资深软件工程师,InfoQ编辑,从事企业级Web应用的相关工作,关注性能优化、Web技术、浏览器等领域。

加载中
1
林希
林希

引用来自“LinkerLin”的答案

非常不看好Go语言,理由:没有C++的基础好,没有Cython方便,没有C通用。也没有Java小白多。
难说啊,这些事情真的很难说啊,不过在我看来在现在的体系结构上, C语言是最佳的语言抽象,很难找到一个层次定位比C语言更好的了,其实这就是一个唯一的问题,就算不是叫C语言,叫其它名字,它的内容表现也是一样的,就不是Denies发明的,也有其它人发明,内容和机制相似。唯一就是唯一。我觉得国防部门的底层开发培训讲的更好,更深。
七液
七液
像C\C++这样的底层语言就不应该掺杂这么多高级特性,C++现在越搞越像动态语言了。这些底层语言就应该保证编码风格统一,实现方式使用最简单的语句稳定才是第一位的,然后高严格编码规范。工业标准,军工标准最好有个这样的部门。随着一代一代的C89,C99,C11诞生我觉得也不能总是用十几年前的编码方法了。
1
刘小羊
刘小羊
不用go的飘过~
1
七液
七液

引用来自“LinkerLin”的答案

非常不看好Go语言,理由:没有C++的基础好,没有Cython方便,没有C通用。也没有Java小白多。

几乎任何一门新的语言诞生的时候,都会被许多人抵制。

包括操作系统,XP有的时候有人抵制,win7有了当初抵制xp这帮人又开始抵制win7了。

go语言要是可以同时保证动态语言的特性和高执行率就会被支持。

C++的基础没觉得多好。现在的C++一味的添加动态语言特性,却留下大量的烂摊子让程序员去解决。然后C++er还特别享受这种折磨,本该是编译器和语言解决的事情现在全部扔给程序员来处理了。增加高级特性却没有增加相应的RTL基础。C++现在已经不再适合多核环境下的开发和跨平台,跨服务开发。

有新的语言诞生的确是一件不错的事情。时间会解释一切的。守旧的人看为什么守。新技术的诞生自然有自己的优点。我觉得go语言还不足以满足未来软件开发需求。但是至少是件好事。编译器领域至少有人想着这事。C++都快三十年了好么。也该换换了

七液
七液
回复 @唐海康 : 他底层解决了许多资源回收的代码。动态处理各种类型。怎么说呢go还在进化中。现在学来有点早。大部分人都是忽悠的一时新鲜就上了,不过有发展总比只维护C++强吧。要居安思危C++的高级特性擦屁股的事情都要自己处理,这些本就是应该编译器搞定的。我支持go这样的新语言探索,不是看好go还有很长路要走。
唐海康
唐海康
怎么这么多人认为go是动态类型的语言啊,我怎么觉得是静态的
七液
七液
回复 @egmkang : closure目前只有apple一家搞搞吧?C++0x里面提供了lambda,还有其他的一些库和特性。这些很明显都是从动态语言中抄袭过去的。以C++那德行,每次引入新特性后总会留下一大堆问题需要程序员自己解决(然后标准委员会的人就出书来介绍这些特性。但是不告诉怎么做是真正的标准,只是告诉你别滥用呀,也别不用呀)学院派就这德行。把简单的搞得复杂化
egmkang
egmkang
C++有啥动态语言的特性? 闭包??
洲宝
洲宝
很赞同,东西都是出来之后慢慢 发展才好的 Go 才出来1.0版本。。还有待提高的地方很多!有有待完善, 也还是不成熟。。 但是经过人们在大量的实践之后才能更好的去发展他!
0
LinkerLin
LinkerLin
非常不看好Go语言,理由:没有C++的基础好,没有Cython方便,没有C通用。也没有Java小白多。
0
灵剑子
灵剑子

go语言的设计者不懂得支持事务的应用程序设计,因此对“发生错误,然后丢掉状态”极度紧张,认为这总是错误的。也不懂得多角度的去看待“异常”(辩证地)。一个网络异常,对于框架或者基础设施而言,他可能是必须处理的“业务”,而对于建模现实业务的代码而言,则褪变成了真正的“异常”,属于部署方面的问题,与被太阳黑子翻转的内存bit是同一类的,这些异常应该作为一个“方面”进行解决,而不是处处交杂在业务代码中。

go的两种错误处理机制,不能够方便地在两种视角上进行转换,并且相对于与标准的异常机制而言,增加了语言的复杂性。

go最初鼓吹是正交的,但go是我所见的最不正交的语言。

go之所以提出panic,其实是后来一种不自信的补救。

whatcq
whatcq
果然 引发争议 看来要系统学习下错误处理的东西
七液
七液
你所说的增加语言复杂性完全是扯淡,几乎没有一本书告诉你如何正确使用异常处理,虽然举了一大堆例子,但是总是告诉你要合理使用不可滥用(什么叫合理什么叫滥用根本没人说过)完全就是学院派的那种理想主义,太阳黑子如果引发问题的话你就是用了异常处理也捕获不到错误,硬件都出错了软件还不出错?现在异常嵌套异常的更恶心,大家还要知道异常抛出格式和类型。
七液
七液
如果JVM出错了a = 100;这样一般看来根本不会出错的语句也会崩溃的.毕竟他需要解释执行虚拟机无法保证所有代码都解释执行成功,go是编译型语言.除非CPU有问题否则根本不会出现mov eax, 100出错的事情.再说现在那些玩溢出的都是在覆盖异常返回点。自己抛自己接有意思么?效率还低。更无法跨语言跨服务跨平台。最后还是回到RPC上。RPC也是返回值处理方式。
七液
七液
开发go语言那两位写代码的时间比你年龄都大,取得的成就也比你认识(也认识你)的所有程序员加起来都大。人家不懂事务处理?再说异常是怎么返回信息的?不也是抛出一个异常里面夹杂着返回信息么。在其他帖子也说过了。虚拟机,动态语言都是不可预料错误的。所以必须用异常。
0
Lunar_Lin
Lunar_Lin

       暂时没看懂, 如果忽略错误且错误真发生的时候会导致panic().   那的确是非常糟糕的设计.   比如说C++的异常说明 就是非常糟糕的, 因为一旦不符合异常说明 就会调用panic(). 这简直是恐怖主义.

七液
七液
异常处理不去捕获和操作都会引发崩溃的事情。而且go没有提倡你使用异常处理,甚至搞出多个返回值来帮你处理这些问题。go没必要搞异常处理。直接返回值就足够了,返回值不if不会崩溃,异常不处理那就崩溃到姥姥家了。C++还鼓励用异常我是难以理解,底层代码都崩溃了你指望谁给你擦屁股呢?
0
铂金胖子
铂金胖子
go 语言 接近c,学学去。
0
戴威
戴威

我也非常不习惯go语言的异常处理机制。


0
永远在一起

有人能说明一下异常和错误有什么区别么?程序什么时候要抛出异常,什么时候返回error。

panic/recover机制和throw/catch 有什么区别。throw不是最后也终止程序,catch可以让throw终止程序么?panic/recover不是和这个很像??有什么区别

whatcq
whatcq
我感觉像 法律上的 责任认定。。。
chunquedong
chunquedong
回复 @七液 : 在函数调用的最顶层捕获异常,应该不会崩溃的。
七液
七液
回复 @chunquedong : 你可以调试找出错误点直接处理错误返回值就好了。你不处理也不会造成服务崩溃。只要有一个if处理了这个返回值,就可以知道运行出错了。然后做相应的处理即可。函数执行出错是可以被允许的没有人说一个函数执行肯定会绝对正确,但是异常处理要是有一个没有捕获。这个服务就完蛋了。你认为写if和写try有什么区别?我觉得除了try效率更低,多线程不稳定以外也没什么区别。
七液
七液
回复 @chunquedong : 在并行开发中单一客户出错是可以允许的,出错就抛弃这个客户即可,路由器抛弃数据包,网站断开或者拒绝一个用户也很平常,重要的是服务端自己不能崩溃。错误扩大就抛弃这个客户即可,可是异常处理能如何?最后不还是要断开客户么。而且你要是不处理这个异常。整个服务就崩溃了。对于7*24这种级别的服务来说崩溃就什么都完了。
chunquedong
chunquedong
回复 @七液 : 忽略了返回错误后,下面的也跟着出错会导致错误扩散,调试的时候会找不到出错点。 更悲剧的事情是下面接着的代码没有跟着出错,在忽略了出错信息后居然的到了看似正确的结果。
下一页
0
宏哥
宏哥
未经证明的技术都是扯淡
返回顶部
顶部