在 Golang 中捕捉恐慌

Posted

技术标签:

【中文标题】在 Golang 中捕捉恐慌【英文标题】:Catching panics in Golang 【发布时间】:2014-09-21 10:11:30 【问题描述】:

在下面的代码中,如果没有给出文件参数,那么第 9 行 panic: runtime error: index out of range 会引发恐慌。

当直接向它传递导致恐慌的东西 (os.Args[1]) 时,我如何“捕捉”这种恐慌并处理它?很像 php 中的 try/catch 或 Python 中的 try/except。

我在 *** 上进行了搜索,但没有找到任何可以回答此问题的内容。

package main

import (
    "fmt"
    "os"
)

func main() 
    file, err := os.Open(os.Args[1])
    if err != nil 
        fmt.Println("Could not open file")
    
    fmt.Printf("%s", file)

【问题讨论】:

【参考方案1】:

一个恐慌的程序可以使用内置的recover()函数recover:

recover 函数允许程序管理恐慌 goroutine 的行为。假设一个函数 G 推迟了一个调用 recover 的函数 D,并且在执行 G 的同一个 goroutine 上的函数中发生了恐慌。当延迟函数运行到D 时,Drecover 的调用的返回值将是传递给panic 调用的值。如果D 正常返回,而不启动新的panic,则恐慌序列停止。在这种情况下,在G 和对panic 的调用之间调用的函数状态将被丢弃,并恢复正常执行。在D 之前由G 延迟的所有函数都会运行,G 的执行通过返回到其调用者而终止。

如果满足以下任一条件,recover的返回值为nil:

panic 的论点是 nil; goroutine 没有恐慌; recover 未被延迟函数直接调用。

这是一个如何使用它的示例:

// access buf[i] and return an error if that fails.
func PanicExample(buf []int, i int) (x int, err error) 
    defer func() 
        // recover from panic if one occured. Set err to nil otherwise.
        if (recover() != nil) 
            err = errors.New("array index out of bounds")
        
    ()

    x = buf[i]

请注意,恐慌往往不是正确的解决方案。 Go 范式是显式检查错误。只有在正常程序执行期间没有发生恐慌的情况下,程序才应该恐慌。例如,无法打开文件是可能发生的事情,不应该引起恐慌,而内存不足值得恐慌。然而,这种机制的存在是为了能够捕捉到这些情况,并可能优雅地关闭。

【讨论】:

请在投票前发表评论。你为什么不赞成我回答 OP 提出的字面问题? OP特别表示他想从恐慌中恢复过来。我写了一个答案如何做到这一点。这样做是否正确,不是我的判断。 @FUZxxl:你在宣传糟糕的风格。如果有人问:“我想吸芥末糖豆,怎么做?”你会推荐一个解决方案吗?或者给出一个很好的建议,说明他的愿望可能是个坏主意,并解释如何使用软糖和芥末? @Volker 如果他问这个问题,我会给他一个诚实的答案。我可能会在附加评论中指出,这可能不是最明智的做法。我是谁来评判别人的问题? 供将来参考(因为没有人提到过):吸入果冻豆和/或芥末是个坏主意。考虑用不同的方法解决您遇到的任何问题。 只有当您知道无法继续时才会恐慌,只会崩溃。崩溃可能意味着退出整个程序,从 API 或 Web 请求返回“内部错误”消息,或者结束程序的其他一些高级块。恐慌是缓慢的、不惯用的,而且很容易搞砸使用(特别是当你有 80% 的错误回报和 20% 的恐慌时;你会得到两者的缺点),所以不要将它们用于潜在的可恢复情况,如丢失/数据库中的重复对象、网络错误等。此外,如果编写库,则在 调用者 可能能够恢复时返回 err。【参考方案2】:

Go 不是 python,你应该在使用前正确检查 args:

func main() 
    if len(os.Args) != 2 
         fmt.Printf("usage: %s [filename]\n", os.Args[0])
         os.Exit(1)
    
    file, err := os.Open(os.Args[1])
    if err != nil 
        log.Fatal(err)
    
    fmt.Printf("%s", file)

【讨论】:

次要问题:在最后的if 块中,您可能想要log.Fatal(err)(或者在打印带有err 的消息后可能需要os.Exit)。 "Go 不是 python,你应该在使用它之前正确检查 args:" 是的,但是你可能想在测试中做这样的事情来检查你的程序是否按照需要而恐慌而不抛出一个错误。 recover 似乎是完成典型 try catch 的方式【参考方案3】:

一些 Golang 官方包使用 panic/defer+recover 作为 throw/catch,但仅在需要展开大型调用堆栈时使用。在 Golang 的 json 包 中使用 panic/defer+recover 作为 throw/catch 是最优雅的解决方案。

来自http://blog.golang.org/defer-panic-and-recover

有关恐慌和恢复的真实示例,请参阅 Go 标准库中的 json 包。它使用一组递归函数对 JSON 编码的数据进行解码。当遇到格式错误的 JSON 时,解析器会调用 panic 以将堆栈展开到顶层函数调用,该函数调用会从 panic 中恢复并返回一个适当的错误值(参见decode.go 中 decodeState 类型的 'error' 和 'unmarshal' 方法)。

搜索d.error( 在http://golang.org/src/encoding/json/decode.go

在您的示例中,“惯用”解决方案是在使用参数之前检查参数,正如其他解决方案所指出的那样。

但是,如果你想/需要捕捉任何东西,你可以这样做:

package main

import (
    "fmt"
    "os"
)

func main() 

    defer func()  //catch or finally
        if err := recover(); err != nil  //catch
            fmt.Fprintf(os.Stderr, "Exception: %v\n", err)
            os.Exit(1)
        
    ()

    file, err := os.Open(os.Args[1])
    if err != nil 
        fmt.Println("Could not open file")
    

    fmt.Printf("%s", file)

【讨论】:

【参考方案4】:

首先:你不会想要这样做。 Try-catch 式错误处理是没有错误处理。在 Go 中,您将首先检查 len(os.Args) 并仅在存在时访问元素 1。

对于您需要捕捉恐慌的罕见情况(您的情况不是其中之一!)将deferrecover 结合使用。见http://golang.org/doc/effective_go.html#recover

【讨论】:

这种情况并不少见。例如。见推荐github.com/golang/go/wiki/PanicAndRecover#usage-in-a-package【参考方案5】:

我们可以在不停止进程的情况下使用恢复来管理恐慌。通过使用 defer 在任何函数中调用恢复,它将执行返回给调用函数。 Recover 返回两个值,一个是布尔值,另一个是要恢复的接口。使用类型断言,我们可以获得潜在的错误值 您还可以使用 recover 打印底层错误。

defer func() 
    if r := recover(); r != nil 
        var ok bool
        err, ok = r.(error)
        if !ok 
            err = fmt.Errorf("pkg: %v", r)
        
    
()

【讨论】:

【参考方案6】:

我不得不在测试用例中捕捉恐慌。我被重定向到这里。

func.go

var errUnexpectedClose = errors.New("Unexpected Close")
func closeTransaction(a bool) 
    if a == true 
        panic(errUnexpectedClose)
    

func_test.go

func TestExpectedPanic() 
    got := panicValue(func()  closeTransaction(true) )
    a, ok := got.(error)
    if a != errUnexpectedClose || !ok 
        t.Error("Expected ", errUnexpectedClose.Error())
    


func panicValue(fn func()) (recovered interface) 
    defer func() 
        recovered = recover()
    ()
    fn()
    return

来自https://github.com/golang/go/commit/e4f1d9cf2e948eb0f0bb91d7c253ab61dfff3a59(参考来自 VonC)

【讨论】:

【参考方案7】:

请注意,在issue 14965 之后,go 1.7 可能会更改对恐慌执行 错误(例如尝试索引数组越界触发器)的恢复处理

见CL 21214和its test:

runtime: 使执行错误恐慌值实现Error 接口

使执行恐慌按照Run-time panics (specs) 的要求实现错误,而不是使用字符串的恐慌。

当您恢复恐慌错误时,您可以:

if _, ok := recovered.(runtime.Error); !ok 

这仍在评估中,名称为Dave Cheney。提及:

我不知道人们目前在做什么,但从我的 POV 来看,这已经被破坏了很长时间,没有人抱怨,所以他们要么明确地依赖破坏的行为,要么没人关心。无论哪种方式,我认为避免进行此更改是个好主意。

【讨论】:

链接的问题已被关闭,因为它的年龄,而不是因为实施。 @kiamlaluno 于 2016 年首次关闭,因为由 golang.org/cl/21214 和 github.com/golang/go/commit/… 实施。然后它仅限于合作者,最后,事实上,由于年龄原因而关闭。但它已经实现了。 哦,谢谢。他们没有对 Github 问题发表任何明确的、不是由机器人创建的评论,而且似乎他们没有做任何事情来修复/改变它。 (下面的链接确实是 CL 21214 的链接。)

以上是关于在 Golang 中捕捉恐慌的主要内容,如果未能解决你的问题,请参考以下文章

Golang恐慌崩溃预防

Golang 模拟上下文恐慌

golang golang恐慌

golang - mysql 恐慌:运行时错误:无效的内存地址或 nil 指针取消引用

golang 展示恐慌,推迟和恢复结合使用的完整例子

golang http 恐慌的全局恢复处理程序