为啥恐慌恢复以局部变量返回值不起作用?

Posted

技术标签:

【中文标题】为啥恐慌恢复以局部变量返回值不起作用?【英文标题】:Why does go panic recover to return value with local variable not work?为什么恐慌恢复以局部变量返回值不起作用? 【发布时间】:2021-10-03 20:59:05 【问题描述】:

这个panic recover code 使用命名的返回值。

func main() 
    result, err := foo()
    fmt.Println("result:", result)
    if err != nil 
        fmt.Println("err:", err)
    


func foo() (result int, err error) 
    defer func() 
        if e := recover(); e != nil 
            result = -1
            err = errors.New(e.(string))
        
    ()
    bar()

    result = 100
    err = nil
    return


func bar() 
    panic("panic happened")

输出

result: -1
err: panic happened

但是为什么带有局部变量的this code 不起作用呢?

func main() 
    result, err := foo()
    fmt.Println("result:", result)
    if err != nil 
        fmt.Println("err:", err)
    


func foo() (int, error) 
    var result int
    var err error
    defer func() 
        if e := recover(); e != nil 
            result = -1
            err = errors.New(e.(string))
        
    ()
    bar()

    result = 100
    err = nil
    return result, err


func bar() 
    panic("panic happened")

输出

result: 0

有什么解释可以帮助我理解它的原因/基本概念吗?在 go tour basics 中解释如下。

命名返回值 Go 的返回值可以命名。如果是这样,它们将被视为在函数顶部定义的变量。

所以应该是一样的吧?

【问题讨论】:

你可以得到A -> B,但不能得到B -> A。 “所以它应该是一样的,对吧?”,不,对于延迟函数来说根本不是。在第二个示例中,local 变量 err 已更改,但这不会传播回调用者。 @Volker:当你的意思是 local 变量 err 在第二种情况下,这是否意味着在第一种情况下命名返回,值(结果,错误)做在 main() 中有参考吗?试图了解命名返回如何在后台工作 有一种技术方法可以将返回的值传播回调用者:堆栈上的插槽或寄存器或......无关紧要。 Named 返回值允许访问此插槽。命名的返回值仅使用该插槽作为内存。当您执行 return result, err 时,err复制到该插槽中(也是结果),但 err 有它自己的内存。 【参考方案1】:

Spec: Return statements 详细说明:

从具有结果类型的函数返回值有三种方式:

    一个或多个返回值可能在“return”语句中明确列出。每个表达式都必须是单值的,并且可以分配给函数结果类型的相应元素。 “return”语句中的表达式列表可能是对多值函数的一次调用。效果就好像从该函数返回的每个值都分配给具有相应值类型的临时变量,然后是列出这些变量的“return”语句,此时适用前一种情况的规则。 如果函数的结果类型为其结果参数指定名称,则表达式列表可能为空。结果参数充当普通的局部变量,函数可以根据需要为它们赋值。 “return”语句返回这些变量的值。

因此,基本上如果您使用显式列出返回值的return 语句,则无论结果参数是否命名,都将使用这些语句。

如果结果参数被命名,它们充当普通的局部变量:您可以读取和写入它们。 如果结果参数被命名,您可以使用“裸”return 语句,而不列出要返回的值。如果这样做,那么实际的返回值将是(命名的)结果参数的值。 如果您的函数由于恐慌和恢复而没有达到return 语句,同样的情况也适用:一旦延迟函数运行,实际返回值将是命名结果参数的值(即延迟函数可以改变并“有发言权”返回)。

如果你不使用命名结果参数但你声明了局部变量,它们这种方式并不特殊:当函数返回时,那些不使用“自动”作为结果值(就像它们将被命名为结果参数而不是局部变量一样)。因此,如果您在延迟函数中更改它们,则不会对返回的实际值产生任何影响。事实上,如果你不使用命名的结果参数并且你的函数恐慌和恢复,你不能指定返回值,它们将是结果类型的zero values。这就是为什么你看到result: 00int 的零值)并且没有错误(因为error 是一个接口类型并且接口类型的零值是nil 并且你不打印错误如果是nil)。

查看相关:How to return a value in a Go function that panics?

【讨论】:

【参考方案2】:

请注意,这与恐慌/恢复无关,它是defer 语句的一个特性。

... 如果延迟函数是 函数字面量 和周围的 函数具有命名结果参数,它们在范围内 字面量,延迟函数可以访问和修改结果 参数之前他们被返回。如果延迟函数有 任何返回值,当函数完成时它们将被丢弃。

【讨论】:

您能否在回答中反映上述问题的逻辑?因为我看到result 在第二种情况下确实被修改了,但是由于返回值被丢弃,结果的值仍然是 0? @Inian bar() 恐慌,所以result = 100 不会被执行。 recover 不会自动导致恐慌函数从中断处继续。【参考方案3】:

可能是@icza 的anwser 的简短摘要:

    命名的返回变量在函数没有恐慌的情况下终止(正常返回或从恐慌中恢复)时使用其最终值返回,因此您可以在 defer recover func() 中更改它们,并且最终值已更改,因此返回价值观。 如果使用局部变量,编译器无法知道这些局部变量将用作返回变量,直到正常返回。局部变量可能会在恐慌恢复中改变,但是 由于 panic,return 语句尚未执行,所以你定义的局部变量没有被视为返回变量,返回值将是返回类型的零值。

【讨论】:

这没有提供问题的答案。一旦你有足够的reputation,你就可以comment on any post;相反,provide answers that don't require clarification from the asker。 - From Review

以上是关于为啥恐慌恢复以局部变量返回值不起作用?的主要内容,如果未能解决你的问题,请参考以下文章

将局部变量更改为全局变量不起作用

当指针指向数组时,为啥 operator(*) 的值不起作用?

为啥链接元素中的媒体属性对打印值不起作用? [复制]

为啥这个 PL/SQL 过程不起作用?

最小值不起作用

VueJS从Vuex返回值不起作用