Panic,堆栈跟踪以及如何恢复【最佳实践】(译文)

作者:Stefan Nilsson

原文网址: yourbasic.org/golang/reco…

Panic 是 Go 中的一个异常

Panics 类似于 C++ 和 Java 异常,但仅适用于运行时错误,例如跟随一个 nil 指针或试图对数组访问超出范围的索引。为了表示诸如文件结束之类的事件,Go 程序使用内置 error 类型。有关错误的更多信息,请参见 错误处理最佳实践3种创建错误的简单方法

Panic 停止 goroutine 的正常执行

  • 程序出现 panic 时,它将立即开始展开调用堆栈。
  • 一直持续到程序崩溃并打印堆栈跟踪,
  • 或直到调用内置的恢复功能。

panic 是由运行时错误或对内置函数 panic 的显式调用引起的。

堆栈跟踪记录

堆栈跟踪记录 —— 所有活动堆栈帧的报告 —— 通常在 panic 发生时将其打印到控制台。堆栈跟踪对于调试非常有用:

  • 您不仅可以看到错误发生的地方,
  • 而且可以看到程序是如何到达这个地方的。

解释堆栈跟踪

这是一个堆栈跟踪的示例:

goroutine 11 [running]:
testing.tRunner.func1(0xc420092690)
    /usr/local/go/src/testing/testing.go:711 +0x2d2
panic(0x53f820, 0x594da0)
    /usr/local/go/src/runtime/panic.go:491 +0x283
github.com/yourbasic/bit.(*Set).Max(0xc42000a940, 0x0)
    ../src/github.com/bit/set_math_bits.go:137 +0x89
github.com/yourbasic/bit.TestMax(0xc420092690)
    ../src/github.com/bit/set_test.go:165 +0x337
testing.tRunner(0xc420092690, 0x57f5e8)
    /usr/local/go/src/testing/testing.go:746 +0xd0
created by testing.(*T).Run
    /usr/local/go/src/testing/testing.go:789 +0x2de
复制代码

可以从下至上阅读:

  • testing.(*T).Run 调用了 testing.tRunner ,
  • testing.tRunner 调用了 bit.TestMax ,
  • bit.TestMax 调用了 bit.(*Set).Max ,
  • bit.(*Set).Max 调用了 panic ,
  • panic 调用了 testing.tRunner.func1

缩进的行显示了调用该函数的源文件和行号。十六进制数字表示参数值,包括指针和内部数据结构的值。 Go 中的堆栈跟踪 具有更多详细信息。

打印并记录堆栈跟踪

要打印当前 goroutine 的堆栈跟踪,请使用包 runtime/debug 中的 debug.PrintStack

您还可以通过调用 runtime.Stack 以编程方式检查当前的堆栈跟踪

详细程度

变量 GOTRACEBACK 控制 Go 程序失败时生成的输出量。

GOTRACEBACK = none
GOTRACEBACK = single
GOTRACEBACK = all
GOTRACEBACK = system

恢复和捕获 Panic

内置的 recover 函数可用于重新获得对异常程序的控制并恢复正常执行。

  • 调用 recover 将停止展开并返回传递给 panic 的参数。
  • 如果 goroutine 没有异常,则恢复将返回 nil。

因为展开时运行的唯一代码是在 defer 函数内部,所以 recover 仅在此类函数内部有用。

Panic 处理程序示例

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    m := 1
    panic("foo: fail")
    m = 2
    return m
}
复制代码
foo: fail
main received 0
复制代码

由于 panic 是在 foo 返回值之前发生的,因此 n 仍然具有其初始零值。

返回值

要在发生 panic 时返回值,必须使用命名返回值。

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() (m int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
            m = 2
        }
    }()
    m = 1
    panic("foo: fail")
    m = 3
    return m
}
复制代码
foo: fail
main received 2
复制代码

测试 Panic(实用功能)

在此示例中,我们使用反射来检查接口变量列表是否具有与给定函数的参数相对应的类型。如果是这样,我们使用这些参数调用该函数以检查是否有 panic。

// Panics tells if function f panics with parameters p.
func Panics(f interface{}, p ...interface{}) bool {
    fv := reflect.ValueOf(f)
    ft := reflect.TypeOf(f)
    if ft.NumIn() != len(p) {
        panic("wrong argument count")
    }
    pv := make([]reflect.Value, len(p))
    for i, v := range p {
        if reflect.TypeOf(v) != ft.In(i) {
            panic("wrong argument type")
        }
        pv[i] = reflect.ValueOf(v)
    }
    return call(fv, pv)
}

func call(fv reflect.Value, pv []reflect.Value) (b bool) {
    defer func() {
        if err := recover(); err != nil {
            b = true
        }
    }()
    fv.Call(pv)
    return
}
复制代码

扫描下方二维码,关注 Feed , 定期推送最新随笔