为什么Go语言除了错误处理还加入了panic和recover?

15
为什么Go语言最终采用了panic/recover异常处理机制,尽管该语言在错误代码方面是如此典型且强烈支持?Go语言的设计者们预见到哪些场景不能通过错误代码处理并需要使用panic/recover呢?
我知道惯例说要限制panic/recover的使用,但运行时是否也会限制它们的使用方式,使它们不能像C++中的throw/catch一样被广泛使用?

3
我既赞同你的问题,也投票支持关闭它:虽然它合理而且有趣,但它违反了SO的一个准则——问题不应引起基于主观看法的答案。我建议你去Go邮件列表上提出这种类型的问题。 - kostix
2
如果我们用“什么是...的用途”来替换“为什么Go添加了...”,那么这就成为一个客观可回答的问题。 - biziclop
3
@zlatanski,你忽略的是:1)SO并不是获取编程主题知识的唯一媒介。它的FAQ中清楚地阐述了一组目标,而你决定不喜欢这些目标,这是你的问题,而不是SO的问题;2)是你自己决定违反规则。因此,指责我是令人惊讶的:假设你从美国搬到英格兰,但仍决定在道路的右侧开车,那么你会责怪警察吗? - kostix
4个回答

19

一些历史:

在Go语言的早期版本(1.0之前),没有recover()。调用panic()会终止应用程序,没有任何方法可以停止它。

我找到了最初讨论添加recover()的帖子,你可以在golang-nuts讨论区阅读:

提议添加类似异常机制

注意:该讨论始于2010年3月25日,非常冗长(6页150个帖子)。

最终,它于2010-03-30被添加:

This release contains three language changes:
1. The functions panic and recover, which are used for reporting and recovering from failures, have been added to the specification. For more information, please refer to http://golang.org/doc/go_spec.html#Handling_panics.
2. In a related change, panicln has been removed, and panic is now a function that takes only one argument.
3. The gc compilers recognize panic and recover, but the new behavior has not yet been implemented.
多返回值和惯例为在Go中处理错误提供了更清晰的方式。这并不意味着在某些(罕见)情况下,panic-recover是无用的。引用官方FAQ:为什么Go没有异常?的话:“Go还有一些内置函数来信号和从真正异常情况中恢复。恢复机制仅作为函数状态在错误后被撤销的一部分执行,这足以处理灾难,但不需要额外的控制结构,并且当使用得当时,可以产生干净的错误处理代码。”以下是一个“现实生活”示例,说明何时/如何使用它是有用的:引用博客文章延迟、恐慌和恢复中的内容:
在现实世界中,关于panic和recover的一个例子是Go标准库中的json包。它使用一组递归函数解码JSON编码的数据。当遇到格式错误的JSON时,解析器调用panic来展开堆栈到顶层函数调用,然后从panic中恢复并返回适当的错误值(请参见decode.go中decodeState类型的'error'和'unmarshal'方法)。
Another example is when you write code (e.g. package) which calls a user-supplied function. You can't trust the provided function that it won't panic. One way is not to deal with it (let the panic wind up), or you may choose to "protect" your code by recovering from those panics. A good example of this is the http server provided in the standard library: you are the one providing functions that the server will call (the handlers or handler functions), and if your handlers panic, the server will recover from those panics and not let your complete application die.
How you should use them:
The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.
Related and useful readings:

http://blog.golang.org/defer-panic-and-recover

http://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right

https://golang.org/doc/faq#exceptions

http://evanfarrer.blogspot.co.uk/2012/05/go-programming-language-has-exceptions.html


谢谢您的回答。我已经阅读了FAQ和博客,但它们存在冲突。FAQ中提到了"真正异常的情况",而博客则是指当遇到"格式错误的JSON时...展开堆栈",因此基本上是在处理不良JSON时使用它来控制流程(这并没有给我留下什么"真正异常的情况"的感觉,就像某些意外的操作系统错误一样,只是用户输入错误)。 - zlatanski
2
@zlatanski,不,对于你所提到的情况使用panic显然是滥用该功能。但问题在于,以那种方式滥用它比检查标志等方式使代码更可读/可理解和可维护。放弃一些纯粹性并在可维护性方面取胜比反过来要好;-) - kostix
3
谢谢你挖掘历史,非常具有洞察力。 - zlatanski

1

我认为你的问题是由于你持有的心理模型造成的,这种心理模型是由流行的主流语言(如Java、C++、C#、PHP等)灌输的,它们简单地处理了异常。

事实上,异常本身并不是一个错误的概念,但滥用它们来处理现实中并非异常的情况是错误的。我个人最讨厌的是Java的文件系统处理API(以及几乎照搬Java的.NET):如果文件不存在,为什么打开文件失败会导致异常呢?文件系统是一种固有的竞争性媒介,并且被指定为具有竞争性,因此在打开文件进行读取之前,确保文件存在的唯一正确方法就是打开它,然后检查“文件不存在”错误:文件不存在的情况根本不是异常情况。

因此,Go明确区分了异常情况和普通的正常错误。Go维护的处理错误的立场的座右铭是“错误是值”,因此预期的普通错误被视为值来处理,而panic()用于处理异常情况。以下是一个很好的简单示例:

  • 尝试对nil指针进行解引用会导致panic

    原因:您的代码继续尝试对不指向任何值的指针进行解引用。紧随其后的代码明确期望该值可用,作为解引用操作的结果。因此,控制流不能以任何明智的方式正常进行,这就是为什么这是一个异常情况:在正确的程序中,无法发生对nil指针的解引用。

  • TCP流的远程端突然关闭了它的流一侧,下一次尝试从它读取时会导致错误。

    这是非常正常的情况:人们不能合理地期望TCP会很稳定:网络中断、数据包丢失、意外停电确实会发生,我们必须为远程同行意外流关闭做好准备。

一个微小的变化是,Go不会强制你盲目遵循某些教条,你可以自由地在严格控制的特定情况下“滥用”panic/recover,例如通过使用特定类型的错误值进行panic来从深层嵌套的处理循环中跳出,并在执行recover的位置上检查它。
进一步阅读:

5
因此,Go语言明确区分普通错误和异常情况,但几乎所有情况都取决于上下文,没有清晰的区别。这就是为什么许多编程语言选择将异常作为通用的错误处理机制而不进行人为分离的原因。无论你喜欢与否,这种方法是有效的,并且具有自己非常大的优势——清晰地分离错误处理和实际业务逻辑。而 Go 语言本身无法通过定义来实现这一点——所有的 Go 代码都是由一些 if 条件判断和逻辑片段混合组成的。在简单情况下,它可以工作,但很快就会变得难以阅读。 - creker
@creker,基本上这就是我投票关闭答案的原因:人们对这些问题确实有不同的看法。我知道当我读我的第一本C++书时,我自己也会有“哇!”的反应,但随后的15年行业经验改变了我的想法。我认为我无法更好地总结我的担忧,比我链接到的0MQ作者的帖子更好,所以我邀请你去阅读它。但是,你的方法、优先事项和品味肯定与我的不同。这就是为什么我们都喜欢不同语言的原因;-) - kostix
@creker,我知道我的回应有点轻松,但我们使用的媒介格式(SO评论)不适合进行带有示例和案例的详细讨论。所以,如果你愿意的话,我认为我们可以在我建议OP使用的邮件列表上进行有建设性的讨论。 - kostix
可悲的是,无论是 C# / Java 还是 Go,C++ 异常都不好用(没有异常更糟)。它们在不同情况下都有优点。我们需要一些不同的东西。也许 Rust 可以成为一个解决方案。 - creker
“got exceptions wrong”是一个相当愚蠢的说法。返回错误值或抛出异常实际上是一样的。唯一的区别是:“返回一个值”只返回一个函数,而“抛出异常”一次返回多个函数。 - ceving
显示剩余4条评论

0

我认为原因在于并发模型。Go语言天生就是高度并发的语言,其核心语法也是如此。当某些计算局部化到并发进程失败时,但整个系统仍然可以正常工作,这种情况可以被视为正常。在我看来,panic和recover是关于故障处理而不是异常处理。


0

因为这样更强大,可以创建更具弹性的程序。

在 C 中,您无法从 NULL 指针引用中恢复。

在 Go 中,您可以使用 recover 捕获它。

例如,在 Go 中创建一个 HTTP 服务器,可以捕获处理请求时的 NULL 指针引用并返回错误 500,而不是使整个服务器崩溃。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接