我正在使用 go 中的测试包编写一个函数及其单元测试。如果由于某种原因发生恐慌,我试图从该函数返回错误(如果没有恐慌,则为零)。问题是,由于某种原因,延迟函数总是返回 nil 值作为错误
code.go
func SomeFunction() error{
err := error(nil)
defer func() error {
if r := recover(); r != nil {
err = errors.New("SomeFunction: panicked!")
}
fmt.Println(err==nil)
return err
}()
//code that may panic
panic("test")
return err
}
code_test.go
func TestSomefunction(t *testing.T){
err := SomeFunction()
fmt.Println(err)
}
输出:
false
<nil>
我明确调用 panic 来检查我正在编写的测试。在运行 go test
时,输出表明 err 在 deferred 部分内不是 nil。但它向测试函数返回 nil。我肯定在这里遗漏了一些东西。
答案 0 :(得分:1)
编辑:另见icza's answer到Why can a normal return hide a panic that a named return correctly provides to the caller?
这本质上是 How to return a value in a Go function that panics? 的副本。但是,该问题的答案只是说您必须使用命名返回值,而没有解释为什么必须使用命名返回值。
我自己从来没有发现这里的 Go spec 特别清晰。 return statements 的描述提到了三种从具有结果类型的函数返回值的方法,1 但这里没有明确说明恐慌和恢复。其余部分留给标题为 Handling panics:
的部分 <块引用>在执行函数 F
时,显式调用 panic 或 run-time panic 会终止 F
的执行。任何功能都被推迟了...... [大部分被剪掉了,因为这里不相关]。这种终止序列称为恐慌。
[截图]
recover
函数允许程序管理恐慌 goroutine 的行为。假设一个函数 G
推迟了一个调用 recovery 的函数 D
,并且在 G
正在执行的同一个 goroutine 上的一个函数中发生了恐慌。当延迟函数的运行达到D
时,D
调用recover
的返回值将是传递给panic
调用的值。
所有这些话都告诉我们,我们必须将对 recover
的调用放在我们使用 defer
调用的函数中。这部分已经足够清楚了,而且您已经在自己的代码中完成了:
defer func() error {
if r := recover(); r != nil {
err = errors.New("SomeFunction: panicked!")
}
fmt.Println(err==nil)
return err
}()
你的延迟函数——严格来说是一个闭包——有一个 error
类型的返回值,但是这个返回值被丢弃,因为延迟函数只是在调用时丢弃了值(总是)。你还不如写你的闭包什么都不返回;这会以完全相同的方式发挥作用。
但是您的闭包也捕获名为 err
的外部变量,并将其分配给它。你的目标很明确,我认为:你希望这是包含闭包的函数的返回值。唉,不会的。
现在让我们回到对 panic
-and-recover
的描述:
如果 D
正常返回,没有开始新的恐慌,恐慌序列停止。在这种情况下,在 G
和调用 panic 之间调用的函数状态被丢弃,并恢复正常执行。
这也有点令人困惑。让我们考虑一下。这里的最后一句话是恢复正常执行。但是 recover
被调用只是因为延迟函数正在运行。所以正常执行,在这一点上,已经进入了处理 defer
的序列,以相反的顺序,将它们从这个特定函数的 defer
堆栈中弹出。换句话说,我们已经处于从函数 G
(调用 panic
的函数)返回的中间。
在规范进入它自己的例子之前,这里还有一个声明:
<块引用>然后运行 G
之前由 D
延迟的任何函数,并且 G
的执行通过返回其调用者而终止。
这只是告诉我们,实际上,剩余的 defer
会从堆栈中一个一个地弹出并运行。函数G
,换句话说,还在正常返回中(不再恐慌)。
短语具有结果类型的函数仅表示返回某些值的函数,而不是例如 func f(args) { ... }
不返回任何值。
我在上面提到过,返回语句描述谈到了从函数返回值的三种方式:
return
语句与表达式列表结合使用。return
语句。这第三种方法很特别。规范没有出来,说是这样,但我们可以推断它,因为没有表达式的 return
只是返回在变量并且这些变量可以在延迟闭包中修改。
最后一部分没有在此处描述,而是在 defer statements 部分中描述。尽管如此,这是事实。由于延迟闭包可以修改返回值,我们就是这样做的。对于调用 recover
的延迟函数也是如此。 没有值的返回语句,例如:
func f() (a int, b string) {
a = 42
b = "what is six times nine"
return
}
设置 a
和 b
然后返回,以及一个带有值的 return
:
func f() (a int, b string) {
return 42, "what is six times nine"
}
做完全一样的事情。也就是说,首先我们赋值,然后我们返回。然后返回值可以被 defer
覆盖。如果延迟调用之一碰巧通过调用 panic
捕获了 recover
,并且该延迟调用是对一个修改变量的闭包,修改发生在赋值之后,因此改变了变量的值,使 return
返回新赋值的值。
如果我们不使用命名返回变量,那么就没有要修改的变量。因此,对于 recover
来更改返回值,唯一可能的方法是使用命名返回变量。