【问题标题】:Why does `defer recover()` not catch panics?为什么 `defer recover()` 不能捕获恐慌?
【发布时间】:2015-06-13 14:46:04
【问题描述】:

为什么调用 defer func() { recover() }() 可以成功恢复恐慌的 goroutine,但调用 defer recover() 却不能?

作为一个简约的例子,这段代码不会恐慌

package main

func main() {
    defer func() { recover() }()
    panic("panic")
}

但是,直接用recover替换匿名函数会发生恐慌

package main

func main() {
    defer recover()
    panic("panic")
}

【问题讨论】:

    标签: go deferred-execution


    【解决方案1】:

    引用内置函数recover()的文档:

    如果在延迟函数外部调用了recover,它将不会停止恐慌序列。

    在您的第二种情况下,recover() 本身就是延迟函数,显然recover() 不会调用自己。所以这不会停止恐慌序列。

    如果recover() 本身会调用recover(),它将停止恐慌序列(但它为什么会这样做呢?)。

    另一个有趣的例子:

    下面的代码也不会恐慌(在Go Playground上试试):

    package main
    
    func main() {
        var recover = func() { recover() }
        defer recover()
        panic("panic")
    }
    

    这里发生的是我们创建了一个函数类型的recover 变量,该变量具有调用内置recover() 函数的匿名函数的值。我们指定调用recover 变量的值作为延迟函数,因此调用内置的recover() 会停止恐慌序列。

    【讨论】:

    • 以及为什么他们以这种方式指定recover():也许允许裸defer recover() 是额外的实现工作,更重要的是,默默地吞下所有panics 是有风险的忽略方式err 是:它让你的程序无法做你想做的事情并且不知道它是什么的问题变得更容易。
    • @twotwotwo 在我的一个应用程序中,我正在写一个频道,如果它关闭了,我不想惊慌。因此defer recover()。整个套路基本都是defer recover(); ch1 <- <- ch2
    • @EthanReesor;注意,但通常规则是“Only the sole/final sender (or some delegate thereof) should ever close a channel”(因此关闭后不发送)。有些事情通过以其他方式发出关闭信号来实现这一点(比如在一端关闭quitcancel 通道,然后在另一端使用select { case <-quit: /* ...do shutdown stuff... */ default: } 检查退出状态)。更严格地说,我无法更改规格;如果你想抓住他们,defer func() { recover() } 是唯一的方法。
    • @EthanReesor 优雅地关闭并行的东西确实值得更多的关注,而不是我现在可以给予它或适合评论;您想做什么以及如何做(关闭频道、close(quit)sync.WaitGroup 等),有时需要针对您的具体情况进行一些思考。
    • @twotwotwo 你有阅读推荐来学习像 Go 这样的语言的良好并行/异步实践吗?也就是说,一种有协程/协程和通道的语言,而不是线程。
    【解决方案2】:

    Handling panic 部分提到

    两个内置函数 panicrecover 有助于报告和处理运行时恐慌

    recover 函数允许程序管理恐慌 goroutine 的行为。

    假设一个函数G 延迟了一个调用recover 的函数D,并且panic 出现在执行G 的同一个goroutine 上的一个函数中。

    当延迟函数的运行达到D时,Drecover的调用的返回值将是传递给panic调用的值。
    如果 D 正常返回,没有开始新的恐慌,恐慌序列停止。

    这说明recover 是要在延迟函数中调用,而不是直接调用。
    恐慌时,“延迟函数”不能是内置的recover(),而是defer statement中指定的。

    DeferStmt = "defer" Expression .
    

    表达式必须是函数或方法调用;它不能加括号。
    内置函数的调用受到限制,如expression statements

    除了特定的内置函数,函数和方法的调用和接收操作都可以出现在语句上下文中。

    【讨论】:

      【解决方案3】:

      观察是这里真正的问题是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 将函数 BODY 或块作用域(而不是函数调用)作为其参数。此时,panicrecover() 返回值可以绑定到特定的堆栈帧,并且任何内部堆栈帧都将具有nil 动态上下文。因此,它看起来像这样:

      func b() {
        defer { if recover() != nil { fmt.Printf("bad") } }
      }
      
      func a() {
        defer {
          b()
          if recover() != nil {
            fmt.Printf("good")
          }
        }
        panic("error")
      }
      

      此时,a() 显然处于恐慌状态,但 b() 并非如此,并且不需要正确实现任何副作用,例如“处于延迟 lambda 的第一个堆栈帧”运行时。

      因此,这里违背了规律:这不能按预期工作的原因是 go 语言中 defer 关键字的设计错误,该关键字是使用不明显的实现细节解决的副作用,然后编成这样。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-10
        • 2014-09-21
        • 2022-11-02
        • 2023-01-26
        • 2011-01-11
        • 1970-01-01
        相关资源
        最近更新 更多