在Golang中捕获panic异常

huangapple go评论158阅读模式
英文:

Catching panics in Golang

问题

使用以下代码,如果没有给出文件参数,则会在第9行panic: runtime error: index out of range抛出一个panic,正如预期的那样。

当直接传递给它(os.Args[1])会导致panic的内容时,我该如何“捕获”并处理它?就像在PHP中的try/catch或Python中的try/except一样。

我在StackOverflow上进行了搜索,但没有找到完全回答这个问题的内容。

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)
}
英文:

With the following code, if no file argument is given, a panic is thrown for line 9 panic: runtime error: index out of range as expected.

How can I 'catch' this panic and handle it when directly when passing something to it (os.Args[1]) that causes the panic? Much like try/catch in PHP or try/except in Python.

I've had a search here on StackOverflow but I've not found anything that answers this as such.

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

得分: 152

一个慌乱的程序可以使用内置的recover()函数来恢复:

recover函数允许程序管理慌乱的goroutine的行为。假设函数G延迟调用了一个调用recover的函数D,并且在执行G的goroutine中的某个函数发生了panic。当延迟函数的执行达到D时,D调用recover的返回值将是传递给panic调用的值。如果D正常返回,没有启动新的panic,则慌乱序列停止。在这种情况下,调用G和调用panic之间调用的函数的状态被丢弃,正常执行恢复。然后运行由GD之前延迟的任何函数,并通过返回到调用者来终止G的执行。

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

  • panic的参数为nil
  • goroutine没有慌乱;
  • recover不是直接由延迟函数调用的。

以下是如何使用它的示例:

// 访问buf[i],如果失败则返回错误。
func PanicExample(buf []int, i int) (x int, err error) {
    defer func() {
        // 如果发生panic,则从中恢复。否则将err设置为nil。
        if recover() != nil {
            err = errors.New("数组索引越界")
        }
    }()

    x = buf[i]
}

请注意,通常情况下,慌乱并不是正确的解决方案。Go的范式是显式地检查错误。只有在普通程序执行期间不会发生导致慌乱的情况下,程序才应该发生慌乱。例如,无法打开文件是可能发生的事情,不应该引起慌乱,而内存耗尽则值得慌乱。尽管如此,这个机制存在是为了能够捕捉甚至这些情况,并可能优雅地关闭程序。

英文:

A panicking program can recover with the builtin recover() function:

> The recover function allows a program to manage behavior of a panicking goroutine. Suppose a function G defers a function D that calls recover and a panic occurs in a function on the same goroutine in which G is executing. When the running of deferred functions reaches D, the return value of D's call to recover will be the value passed to the call of panic. If D returns normally, without starting a new panic, the panicking sequence stops. In that case, the state of functions called between G and the call to panic is discarded, and normal execution resumes. Any functions deferred by G before D are then run and G's execution terminates by returning to its caller.
>
> The return value of recover is nil if any of the following conditions holds:
>
> * panic's argument was nil;
> * the goroutine is not panicking;
> * recover was not called directly by a deferred function.

Here is an example of how to use this:

// 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]
}

Notice that more often than not, panicking is not the right solution. The Go paradigm is to check for errors explicitly. A program should only panic if the circumstances under which it panics do not happen during ordinary program executing. For instance, not being able to open a file is something that can happen and should not cause a panic while running out of memory is worth a panic. Nevertheless, this mechanism exists to be able to catch even these cases and perhaps shut down gracefully.

答案2

得分: 48

Go不是Python,你在使用之前应该正确检查参数:

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)
}
英文:

Go is not python, you should properly check for args before you use it:

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)
}

答案3

得分: 21

一些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'方法)。

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

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

但是,如果你想/需要捕获任何异常,可以这样做:

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)
}
英文:

Some Golang official packages use panic/defer+recover as throw/catch, but only when they need to unwind a large call stack. In Golang's json package using panic/defer+recover as throw/catch is the most elegant solution.

from http://blog.golang.org/defer-panic-and-recover
>For a real-world example of panic and recover, see the json package from the Go standard library. It decodes JSON-encoded data with a set of recursive functions. When malformed JSON is encountered, the parser calls panic to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the 'error' and 'unmarshal' methods of the decodeState type in decode.go).

Search for d.error(
at http://golang.org/src/encoding/json/decode.go

In your example the "idiomatic" solution is to check the parameters before using them, as other solutions have pointed.

But, if you want/need to catch anything you can do:

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

得分: 16

首先,你不会想要这样做。尝试-捕获式的错误处理并不是真正的错误处理方式。在Go语言中,你应该首先检查len(os.Args),只有在存在时才访问第一个元素。

对于那些罕见需要捕获panic的情况(而你的情况不是其中之一!),可以使用deferrecover结合使用。请参考http://golang.org/doc/effective_go.html#recover。

英文:

First: You wouldn't want to do this. Try-catch-style error handling is no error handling. In Go you would check len(os.Args) first and access element 1 only if present.

For the rare cases you need to catch panics (and your case is not one of them!) use defer in combination with recover. See http://golang.org/doc/effective_go.html#recover

答案5

得分: 8

我们可以使用recover来管理恐慌,而不会停止进程。通过在任何函数中使用defer调用recover,它将返回到调用函数的执行。Recover返回两个值,一个是布尔值,另一个是用于恢复的接口。使用类型断言,我们可以获取底层的错误值。
你也可以使用recover打印底层错误。

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

We can manage panic without halting process using recover. By calling recover in any function using defer it will return the execution to calling function. Recover returns two values one is boolean and other one is interface to recover. Using type assertion we can get underlying error value
You can also print underlying error using recover.

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

答案6

得分: 4

我不是一个代码翻译器,但我可以帮你理解这段代码的含义。这段代码是一个Go语言的测试案例,用于测试一个名为closeTransaction的函数。在这个测试案例中,我们期望closeTransaction函数会引发一个恐慌(panic),并且恐慌的值应该是errUnexpectedClose

func.go文件中,定义了一个名为closeTransaction的函数,它接受一个布尔值参数a。如果a的值为true,则会引发一个恐慌,恐慌的值是errUnexpectedClose

func_test.go文件中,定义了一个名为TestExpectedPanic的测试函数。在这个函数中,我们调用了panicValue函数,并传入一个匿名函数func() { closeTransaction(true) }作为参数。panicValue函数的作用是执行传入的函数,并捕获其中引发的恐慌。然后,我们将捕获到的恐慌值与errUnexpectedClose进行比较,如果不相等或者无法转换为error类型,则表示测试失败。

这段代码的来源是GitHub上的一个提交记录,具体链接是https://github.com/golang/go/commit/e4f1d9cf2e948eb0f0bb91d7c253ab61dfff3a59。

英文:

I had to catch panics in a test case. I got redirected here.

> 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
}

Used from https://github.com/golang/go/commit/e4f1d9cf2e948eb0f0bb91d7c253ab61dfff3a59 (ref from VonC)

答案7

得分: 2

请注意,恢复处理恐慌“执行”错误(例如尝试触发数组越界索引)可能会在Go 1.7之后发生变化,参见问题14965

请参阅CL 21214其测试

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

根据运行时恐慌(规范)的规定,使执行恐慌实现Error,而不是带有字符串的恐慌。

当您恢复恐慌错误时,您可以执行以下操作:

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

这仍在评估中,正如Dave Cheney所提到的:

我不知道人们目前在做什么,但从我的角度来看,这个问题已经存在很长时间了,没有人抱怨,所以他们要么明确依赖于错误的行为,要么根本不在意。无论哪种情况,我认为避免进行此更改是一个好主意。

英文:

Note that the recover treatment of a panic Execution error (such as attempting to index an array out of bounds trigger) might change with go 1.7 after issue 14965

See CL 21214 and its test:

> ## runtime: make execution error panic values implement the Error interface
>
> Make execution panics implement Error as mandated by Run-time panics (specs), instead of panics with strings.

When you recover a panic error, you would be able to do:

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

This is still being evaluated, and as Dave Cheney. mentions:

> I don't know what people are currently doing but from my POV this has been broken for a long time and nobody has complained so they are either explicitly relying on the broken behaviour, or nobody cares. Either way I think it's a good idea to avoid making this change.

答案8

得分: 1

在Go语言中,panic并不是惯用的做法,也不应该像其他编程语言中的异常一样使用,但有时确实需要捕获它们。

在这些情况下,我发现定义一个CatchPanic实用程序非常有用,它将panic转换为可检查的error,以尽量减少与Go惯用法的偏离。

例如:

func CatchPanic[T interface{}](f func() T) (ret T, err error) {
    defer func() {
        rec := recover()
        if rec != nil {
            err = errors.New(fmt.Sprint(rec))
        }
    }()
    return f(), nil
}

用法示例(过于简化):

func main() {
    arr := []int{0, 1, 2, 3, 4}
    val, err := CatchPanic(func() int {
        return arr[5] // 会引发panic
    })
    if err != nil {
        // 处理错误
    }
}

lambda参数的返回值可以根据其他用例进行调整。

请注意,在可能的情况下,建议不要依赖Go中的panic捕获,最好在访问之前进行检查。

英文:

While panic in Go isn't idiomatic and shouldn't be used as exceptions in other programming languages, sometimes the need to catch them does arise.

In these cases I've found it useful to define a CatchPanic utility that translate panic into a checkable error to minimize divergence from Go's idioms.

For example:

func CatchPanic[T interface{}](f func() T) (ret T. err error) {
    defer func() {
        rec = recover()
        if rec != nil {
            err = errors.New(fmt.Sprint(rec))
        }
    }()
    return f(), nil
}

Usage example (oversimplified..):

func main() {
    arr := []int{0, 1, 2, 3, 4}
    val, err := CatchPanic(func() int {
        return arr[5] // Will panic
    })
    if err != nil {
        // Handle error
    }
}

The return value of the lambda parameter can be adjusted to other use cases.

Do note that when possible it's advised not to relay on panic catching in Go and check before access is preferable.

huangapple
  • 本文由 发表于 2014年7月30日 05:42:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/25025467.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定