跳转到主要内容

介绍


程序遇到的错误分为两大类:程序员已经预料到的错误和程序员没有预料到的错误。我们在前两篇关于错误处理的文章中介绍的错误接口主要处理我们在编写 Go 程序时所期望的错误。错误接口甚至允许我们确认函数调用发生错误的罕见可能性,因此我们可以在这些情况下做出适当的响应。

恐慌属于第二类错误,这是程序员没有预料到的。这些不可预见的错误导致程序自发终止并退出正在运行的 Go 程序。常见的错误通常是造成恐慌的原因。在本教程中,我们将研究常见操作在 Go 中产生恐慌的几种方式,我们还将看到避免这些恐慌的方法。我们还将使用 defer 语句和恢复函数来捕获恐慌,以免它们有机会意外终止我们正在运行的 Go 程序。

了解恐慌(Panics )


Go 中有某些操作会自动返回恐慌并停止程序。常见操作包括索引超出其容量的数组、执行类型断言、在 nil 指针上调用方法、错误地使用互斥锁以及尝试使用封闭通道。大多数这些情况是由于在编译程序时编译器无法检测到的编程错误造成的。

由于恐慌包含有助于解决问题的细节,因此开发人员通常使用恐慌来表示他们在程序开发过程中犯了错误。

越界恐慌


当您尝试访问超出切片长度或数组容量的索引时,Go 运行时将产生恐慌。

以下示例犯了一个常见错误,即尝试使用 len 内置函数返回的切片长度来访问切片的最后一个元素。尝试运行这段代码,看看为什么会产生恐慌:

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

这将具有以下输出:

Output
panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

恐慌输出的名称提供了一个提示:恐慌:运行时错误:索引超出范围。我们用三种海洋生物创建了一个切片。然后,我们尝试通过使用 len 内置函数用切片长度索引切片来获取切片的最后一个元素。请记住,切片和数组是从零开始的;所以第一个元素为零,这个切片中的最后一个元素位于索引 2 处。由于我们尝试访问位于第三个索引 3 处的切片,因此切片中没有要返回的元素,因为它超出了切片的边界.运行时别无选择,只能终止并退出,因为我们要求它做一些不可能的事情。 Go 也无法在编译期间证明这段代码会尝试这样做,因此编译器无法捕捉到这一点。

另请注意,后续代码没有运行。这是因为恐慌是完全停止执行 Go 程序的事件。生成的消息包含有助于诊断恐慌原因的多条信息。

恐慌的解剖


恐慌由指示恐慌原因的消息和帮助您定位代码中产生恐慌的位置的堆栈跟踪组成。

任何恐慌的第一部分是消息。它总是以字符串 panic: 开头,后面跟着一个字符串,该字符串根据恐慌的原因而变化。上一个练习中的恐慌包含以下信息:

panic: runtime error: index out of range [3] with length 3


字符串运行时错误:在恐慌:前缀之后告诉我们恐慌是由语言运行时生成的。这种恐慌告诉我们,我们试图使用超出切片长度 3 范围的索引 [3]。

此消息之后是堆栈跟踪。堆栈跟踪形成了一张地图,我们可以按照该地图准确定位生成恐慌时正在执行的代码行,以及早期代码如何调用该代码。

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20


这个堆栈跟踪,来自上一个示例,显示我们的程序在第 13 行从文件 /tmp/sandbox879828148/prog.go 生成了恐慌。它还告诉我们这个恐慌是在 main() 函数中从 main包。

堆栈跟踪被分成单独的块——一个用于程序中的每个 goroutine。每个 Go 程序的执行都是由一个或多个 goroutine 完成的,每个 goroutine 可以独立并同时执行部分 Go 代码。每个块都以标头 goroutine X [state]: 开头。标头给出了 goroutine 的 ID 号以及发生恐慌时它所处的状态。在标题之后,堆栈跟踪显示发生恐慌时程序正在执行的函数,以及函数执行的文件名和行号。

上一个示例中的恐慌是由对切片的越界访问产生的。当在未设置的指针上调用方法时,也会产生恐慌。

Nil 接收者(Nil Receivers)


Go 编程语言有指针来引用运行时存在于计算机内存中的某种类型的特定实例。 指针可以假定值 nil 表示它们没有指向任何东西。 当我们尝试在一个为 nil 的指针上调用方法时,Go 运行时将产生一个恐慌。 同样,接口类型的变量在调用方法时也会产生恐慌。 要查看在这些情况下产生的恐慌,请尝试以下示例:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

产生的恐慌将如下所示:

Output
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]

goroutine 1 [running]:
main.(*Shark).SayHello(...)
    /tmp/sandbox160713813/prog.go:12
main.main()
    /tmp/sandbox160713813/prog.go:18 +0x1a

在本例中,我们定义了一个名为 Shark 的结构。 Shark 在它的指针接收器上定义了一个方法,称为 SayHello,当调用它时,它将向标准输出打印一个问候语。在我们的 main 函数的主体中,我们创建了这个 Shark 结构的一个新实例,并使用 & 运算符请求一个指向它的指针。该指针被分配给 s 变量。然后我们使用语句 s = nil 将 s 变量重新赋值为 nil。最后我们尝试在变量 s 上调用 SayHello 方法。我们没有收到来自 Sammy 的友好消息,而是收到了我们试图访问无效内存地址的恐慌。因为 s 变量是 nil,所以当调用 SayHello 函数时,它会尝试访问 *Shark 类型上的字段 Name。因为这是一个指针接收器,并且在这种情况下接收器是 nil,所以它会因为无法取消引用 nil 指针而恐慌。

虽然我们在这个例子中明确地将 s 设置为 nil,但在实践中这种情况不太明显。当您看到涉及 nil 指针取消引用的恐慌时,请确保您已正确分配您可能已创建的任何指针变量。

由 nil 指针和越界访问产生的恐慌是运行时产生的两种常见的恐慌。也可以使用内置函数手动生成恐慌。

使用 panic 内置函数


我们还可以使用 panic 内置函数生成我们自己的恐慌。它需要一个字符串作为参数,这是恐慌将产生的消息。通常,此消息比重写我们的代码以返回错误更简洁。此外,我们可以在我们自己的包中使用它来向开发人员表明他们在使用我们包的代码时可能犯了错误。只要有可能,最佳实践是尝试将错误值返回给我们包的消费者。

运行此代码以查看从另一个函数调用的函数产生的恐慌:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

产生的恐慌输出如下所示:

Output
panic: oh no!

goroutine 1 [running]:
main.foo(...)
    /tmp/sandbox494710869/prog.go:8
main.main()
    /tmp/sandbox494710869/prog.go:4 +0x40

在这里,我们定义了一个函数 foo,它使用字符串 "oh no!" 调用 panic 内置函数。这个函数由我们的主函数调用。请注意输出如何显示消息恐慌:哦,不!堆栈跟踪显示了一个 goroutine,堆栈跟踪中有两行:一行用于 main() 函数,另一行用于我们的 foo() 函数。

我们已经看到恐慌似乎会在它们产生的地方终止我们的程序。当存在需要正确关闭的开放资源时,这可能会产生问题。 Go 提供了一种始终执行某些代码的机制,即使在出现恐慌的情况下也是如此。

延迟函数(Deferred Functions)


您的程序可能具有必须正确清理的资源,即使在运行时正在处理恐慌时也是如此。 Go 允许你推迟一个函数调用的执行,直到它的调用函数完成执行。延迟函数即使在出现恐慌的情况下也会运行,并用作防止恐慌的混乱性质的安全机制。通过像往常一样调用函数来延迟函数,然后在整个语句前加上 defer 关键字,就像在 defer sayHello() 中一样。运行此示例以查看如何在产生恐慌的情况下打印消息:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

此示例产生的输出将如下所示:

Output
hello from the deferred function!
panic: oh no!

goroutine 1 [running]:
main.main()
    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

在本例的 main 函数中,我们首先推迟对匿名函数的调用,该函数会打印消息“hello from the deferred function!”。然后主函数立即使用恐慌函数产生恐慌。在这个程序的输出中,我们首先看到 deferred 函数被执行并打印了它的消息。接下来是我们在 main 中产生的恐慌。

延迟函数可以防止恐慌的令人惊讶的性质。在延迟函数中,Go 还为我们提供了使用另一个内置函数来阻止恐慌终止 Go 程序的机会。

处理恐慌


恐慌有一个单一的恢复机制——恢复内置函数。此函数允许您在通过调用堆栈的过程中拦截恐慌并防止它意外终止您的程序。它对其使用有严格的规定,但在生产应用程序中可能是无价的。

由于它是内置包的一部分,因此无需导入任何其他包即可调用恢复:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

此示例将输出:

Output
2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!


本例中的主函数调用我们定义的函数,divideByZero。在这个函数中,我们推迟了一个匿名函数的调用,该函数负责处理在执行 divideByZero 时可能出现的任何恐慌。在这个延迟匿名函数中,我们调用 recover 内置函数并将它返回的错误分配给一个变量。如果 divideByZero 出现恐慌,则设置此错误值,否则为 nil。通过将 err 变量与 nil 进行比较,我们可以检测是否发生了恐慌,在这种情况下,我们使用 log.Println 函数记录恐慌,就好像它是任何其他错误一样。

在这个延迟的匿名函数之后,我们调用我们定义的另一个函数,划分并尝试使用 fmt.Println 打印其结果。提供的参数将导致 除以执行除以零,这将产生恐慌。

在此示例的输出中,我们首先看到来自恢复恐慌的匿名函数的日志消息,然后是我们幸存下来的消息除以零!。我们确实做到了这一点,这要归功于内建的恢复函数停止了可能会终止我们的 Go 程序的灾难性恐慌。

从recover() 返回的err 值正是提供给panic() 调用的值。因此,确保 err 值仅在没有发生恐慌时才为零是至关重要的。

使用恢复(recover)检测恐慌


恢复函数依赖于错误的值来确定是否发生了恐慌。由于 panic 函数的参数是一个空接口,它可以是任何类型。任何接口类型的零值,包括空接口,都是 nil。必须注意避免将 nil 作为恐慌的参数,如下例所示:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

这将输出:

Output
we survived dividing by zero!


此示例与前面涉及恢复的示例相同,只是稍作修改。 除数函数已更改为检查其除数 b 是否等于 0。如果是,它将使用带有 nil 参数的内置 panic 生成恐慌。 这次的输出不包括显示发生恐慌的日志消息,即使是由除法创建的。 这种静默行为就是为什么确保 panic 内置函数的参数不是 nil 非常重要的原因。

结论


我们已经看到了许多在 Go 中可以创建恐慌的方法,以及如何使用内置函数恢复它们。 虽然您自己可能不一定会使用恐慌,但从恐慌中正确恢复是让 Go 应用程序做好生产准备的重要步骤。

 

文章链接