英文:
Why does `defer recover()` not catch panics?
问题
为什么调用defer func() { recover() }()
可以成功恢复一个发生恐慌的goroutine,而调用defer recover()
则不行?
作为一个简化的例子,下面的代码不会发生恐慌:
package main
func main() {
defer func() { recover() }()
panic("panic")
}
然而,将匿名函数替换为直接调用recover会导致恐慌:
package main
func main() {
defer recover()
panic("panic")
}
英文:
Why does a call to defer func() { recover() }()
successfully recover a panicking goroutine, but a call to defer recover()
not?
As an minimalistic example, this code doesn't panic
package main
func main() {
defer func() { recover() }()
panic("panic")
}
However, replacing the anonymous function with recover directly panics
package main
func main() {
defer recover()
panic("panic")
}
答案1
得分: 15
引用内置函数recover()
的文档说明:
如果在延迟函数之外调用
recover()
,它将无法停止恐慌序列。
在你的第二个例子中,recover()
本身就是延迟函数,显然recover()
不会调用自身。因此,它不会停止恐慌序列。
如果recover()
在自身中调用recover()
,它将停止恐慌序列(但为什么要这样做呢?)。
另一个有趣的例子:
以下代码也不会引发恐慌(在Go Playground上尝试一下):
package main
func main() {
var recover = func() { recover() }
defer recover()
panic("panic")
}
这里发生的是,我们创建了一个函数类型的recover
变量,它的值是调用内置的recover()
函数的匿名函数。我们指定将recover
变量的值作为延迟函数进行调用,因此从其中调用内置的recover()
函数会停止恐慌序列。
英文:
Quoting from the documentation of the built-in function recover()
:
> If recover is called outside the deferred function it will not stop a panicking sequence.
In your second case recover()
itself is the deferred function, and obviously recover()
does not call itself. So this will not stop the panicking sequence.
If recover()
would call recover()
in itself, it would stop the panicking sequence (but why would it do that?).
Another Interesting Example:
The following code also doesn't panic (try it on the Go Playground):
package main
func main() {
var recover = func() { recover() }
defer recover()
panic("panic")
}
What happens here is we create a recover
variable of function type which has a value of an anonymous function calling the built-in recover()
function. And we specify calling the value of the recover
variable to be the deferred function, so calling the builtin recover()
from that stops the panicing sequence.
答案2
得分: 9
处理 panic 部分提到:
两个内置函数
panic
和recover
用于报告和处理运行时 panic。
recover
函数允许程序管理 panic 协程的行为。
假设函数
G
延迟调用了一个调用recover
的函数D
,并且在执行G
的相同协程中的某个函数中发生了 panic。
当延迟函数的执行达到
D
时,D
调用recover
的返回值将是传递给 panic 调用的值。如果D
正常返回,没有引发新的 panic,那么 panic 序列将停止。
这说明 recover
应该在延迟函数中调用,而不是直接调用。
当发生 panic 时,“延迟函数”不能是内置的 recover()
函数,而是在**延迟语句**中指定的函数。
DeferStmt = "defer" Expression.
表达式必须是一个函数或方法调用;不能使用括号括起来。
对于表达式语句,内置函数的调用受到限制。
除了特定的内置函数之外,函数和方法调用以及接收操作可以出现在语句上下文中。
英文:
The Handling panic section mentions that
> Two built-in functions, panic
and recover
, assist in reporting and handling run-time panics
> The recover
function allows a program to manage behavior of a panicking goroutine.
> Suppose a function G
defers a function D
that calls recover
and a panic
occurs in a function on the same goroutine in which G
is executing.
> When the running of deferred functions reaches D
, the return value of D
's call to recover
will be the value passed to the call of panic.
If D returns normally, without starting a new panic, the panicking sequence stops.
That illustrates that recover
is meant to be called in a deferred function, not directly.
When it panic, the "deferred function" cannot be the built-in recover()
one, but one specified in a defer statement.
DeferStmt = "defer" Expression .
> The expression must be a function or method call; it cannot be parenthesized.
Calls of built-in functions are restricted as for expression statements.
> With the exception of specific built-in functions, function and method calls and receive operations can appear in statement context.
答案3
得分: 2
观察到的一个问题是defer
的设计存在问题,因此答案应该是这样的。
为了解释这个答案,defer
目前需要从lambda中获取一层嵌套的堆栈,并且运行时使用这个约束的一个特定副作用来确定recover()
是否返回nil。
以下是一个例子:
func b() {
defer func() { if recover() != nil { fmt.Printf("bad") } }()
}
func a() {
defer func() {
b()
if recover() != nil {
fmt.Printf("good")
}
}()
panic("error")
}
b()
中的recover()
应该返回nil。
在我看来,一个更好的选择是说defer
接受一个函数体或者块作用域(而不是函数调用)作为参数。这样一来,panic
和recover()
的返回值可以与特定的堆栈帧相关联,任何内部堆栈帧都将具有nil
的panic上下文。因此,代码将如下所示:
func b() {
defer { if recover() != nil { fmt.Printf("bad") } }
}
func a() {
defer {
b()
if recover() != nil {
fmt.Printf("good")
}
}
panic("error")
}
这时,很明显a()
处于panic状态,但b()
不是,并且像“处于延迟lambda的第一个堆栈帧中”这样的副作用对于正确实现运行时来说是不必要的。
因此,与常规思路相悖的是:这个不按预期工作的原因是go语言中defer
关键字的设计错误,通过使用非明显的实现细节副作用进行了解决,并将其作为规范确定下来。
英文:
An observation is that the real problem here is the design of defer
and thus the answer should say that.
Motivating this answer, defer
currently needs to take exactly one level of nested stack from a lambda, and the runtime uses a particular side effect of this constraint to make a determination on whether recover()
returns nil or not.
Here's an example of this:
func b() {
defer func() { if recover() != nil { fmt.Printf("bad") } }()
}
func a() {
defer func() {
b()
if recover() != nil {
fmt.Printf("good")
}
}()
panic("error")
}
The recover()
in b()
should return nil.
In my opinion, a better choice would have been to say that defer
takes a function BODY, or block scope (rather than a function call,) as its argument. At that point, panic
and the recover()
return value could be tied to a particular stack frame, and any inner stack frame would have a nil
pancing context. Thus, it would look like this:
func b() {
defer { if recover() != nil { fmt.Printf("bad") } }
}
func a() {
defer {
b()
if recover() != nil {
fmt.Printf("good")
}
}
panic("error")
}
At this point, it's obvious that a()
is in a panicking state, but b()
is not, and any side effects like "being in the first stack frame of a deferred lambda" aren't necessary to correctly implement the runtime.
So, going against the grain here: The reason this doesn't work as might be expected, is a mistake in the design of the defer
keyword in the go language, that was worked around using non-obvious implementation detail side effects and then codified as such.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论