延迟函数在恐慌后返回 nil 值

时间:2020-12-25 18:08:52

标签: go

我正在使用 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。我肯定在这里遗漏了一些东西。

1 个答案:

答案 0 :(得分:1)

编辑:另见icza's answerWhy 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) { ... } 不返回任何值。


规范示例和/或返回值描述中缺少什么

我在上面提到过,返回语句描述谈到了从函数返回值的三种方式:

  1. 您可以将 return 语句与表达式列表结合使用。
  2. 您可以使用返回多值函数的 return 语句。
  3. 您可以拥有一个带有命名返回值的函数。

这第三种方法很特别。规范没有出来,是这样,但我们可以推断它,因为没有表达式的 return 只是返回在变量并且这些变量可以在延迟闭包中修改

最后一部分没有在此处描述,而是在 defer statements 部分中描述。尽管如此,这是事实。由于延迟闭包可以修改返回值,我们就是这样做的。对于调用 recover 的延迟函数也是如此。 没有值的返回语句,例如:

func f() (a int, b string) {
    a = 42
    b = "what is six times nine"
    return
}

设置 ab 然后返回,以及一个带有值的 return

func f() (a int, b string) {
    return 42, "what is six times nine"
}

做完全一样的事情。也就是说,首先我们赋值,然后我们返回。然后返回值可以被 defer 覆盖。如果延迟调用之一碰巧通过调用 panic 捕获了 recover并且该延迟调用是对一个修改变量的闭包,修改发生在赋值之后,因此改变了变量的值,使 return 返回新赋值的值。

如果我们不使用命名返回变量,那么就没有要修改的变量。因此,对于 recover更改返回值,唯一可能的方法是使用命名返回变量。

相关问题