在围棋中处理恐慌

简介

程序遇到的错误分为两大类:程序员已经预料到的和程序员没有预料到的。我们在前两篇关于[error handling]的文章(https://andsky.com/tech/tutorials/handling-errors-in-go)中介绍过的error接口主要处理我们在编写Go程序时预期的错误。错误接口甚至允许我们承认函数调用中发生错误的罕见可能性,因此我们可以在这些情况下做出适当的响应。

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

了解恐慌

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

由于恐慌包括有助于解决问题的细节,开发人员通常使用恐慌作为他们在程序开发过程中犯下错误的指示。

越界恐慌

当您尝试访问超出片长度或数组容量的索引时,GO运行时将生成死机。

下面的示例犯了一个常见的错误,即试图使用len内置函数返回的切片的长度来访问切片的最后一个元素。尝试运行此代码以了解这可能会导致死机的原因:

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7func main() {
 8    names := []string{
 9    	"lobster",
10    	"sea urchin",
11    	"sea cucumber",
12    }
13    fmt.Println("My favorite sea creature is:", names[len(names)])
14}

这将产生以下输出:

1[secondary_label Output]
2panic: runtime error: index out of range [3] with length 3
3
4goroutine 1 [running]:
5main.main()
6    /tmp/sandbox879828148/prog.go:13 +0x20

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

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

恐慌剖析

死机由一条指示死机原因的消息和一个帮助您定位代码中产生死机的位置的Stack trace]组成。

任何恐慌的第一部分都是信息。它将始终以字符串Panic:开头,后面将跟随一个字符串,该字符串根据引发异常的原因而有所不同。上一次练习中的恐慌传达了这样的信息:

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

前缀Panic:后面的字符串Runtime Error:告诉我们,死机是由语言运行时生成的。这种异常情况告诉我们,我们试图使用的索引[3]超出了片段长度`3‘的范围。

此消息后面是堆栈跟踪。堆栈跟踪形成了一个映射,我们可以根据该映射来准确定位在产生死机时正在执行的代码行,以及早期代码是如何调用该代码的。

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

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

堆栈跟踪被分解成单独的块-每个块对应于程序中的每个goroutine。每个Go程序的执行都是由一个或多个goroutine完成的,每个goroutine都可以独立地同时执行Go代码的一部分。每个块都以头部goroutine X [state]:开始。头部给出了goroutine的ID号以及panic发生时的状态。在头部之后,堆栈跟踪显示了程序在死机发生时正在执行的函数,以及文件名和执行函数的行号。

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

无接收器

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

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7type Shark struct {
 8    Name string
 9}
10
11func (s *Shark) SayHello() {
12    fmt.Println("Hi! My name is", s.Name)
13}
14
15func main() {
16    s := &Shark{"Sammy"}
17    s = nil
18    s.SayHello()
19}

由此产生的恐慌将如下所示:

1[secondary_label Output]
2panic: runtime error: invalid memory address or nil pointer dereference
3[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]
4
5goroutine 1 [running]:
6main.(*Shark).SayHello(...)
7    /tmp/sandbox160713813/prog.go:12
8main.main()
9    /tmp/sandbox160713813/prog.go:18 +0x1a

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

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

由空指针和越界访问生成的死机是运行库生成的两种常见的死机。也可以使用内置函数手动生成死机。

使用panic内置函数

我们也可以使用内置的panic函数生成自己的panic。它接受一个字符串作为参数,这是panic将产生的消息。通常情况下,这条消息比重写代码返回错误要简单。此外,我们可以在我们自己的包中使用它来向开发人员指出他们在使用我们的包代码时可能犯了错误。只要有可能,最佳实践就是尝试向包的使用者返回error值。

运行以下代码查看从另一个函数调用的函数生成的死机:

1package main
2
3func main() {
4    foo()
5}
6
7func foo() {
8    panic("oh no!")
9}

生成的panic输出如下所示:

1[secondary_label Output]
2panic: oh no!
3
4goroutine 1 [running]:
5main.foo(...)
6    /tmp/sandbox494710869/prog.go:8
7main.main()
8    /tmp/sandbox494710869/prog.go:4 +0x40

在这里,我们定义了一个函数foo,它使用字符串)函数,一行用于我们的foo()函数。

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

延迟函数

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

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    defer func() {
 7    	fmt.Println("hello from the deferred function!")
 8    }()
 9
10    panic("oh no!")
11}

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

1[secondary_label Output]
2hello from the deferred function!
3panic: oh no!
4
5goroutine 1 [running]:
6main.main()
7    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

在本例的main函数中,我们首先defer一个匿名函数的调用,该函数从deferred函数输出消息hello!. main函数然后使用panic函数立即产生一个panic。在这个程序的输出中,我们首先看到延迟函数被执行并打印其消息。接下来是我们在main中生成的panic。

延期功能提供了保护,使其免受令人惊讶的恐慌。在延迟函数中,Go还为我们提供了使用另一个内置函数来阻止恐慌终止Go程序的机会。

处理突发事件

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

因为它是Builtin包的一部分,所以不需要导入任何额外的包即可调用:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6)
 7
 8func main() {
 9    divideByZero()
10    fmt.Println("we survived dividing by zero!")
11
12}
13
14func divideByZero() {
15    defer func() {
16    	if err := recover(); err != nil {
17    		log.Println("panic occurred:", err)
18    	}
19    }()
20    fmt.Println(divide(1, 0))
21}
22
23func divide(a, b int) int {
24    return a / b
25}

此示例将输出:

1[secondary_label Output]
22009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
3we survived dividing by zero!

本例中的main函数调用我们定义的函数DivideByZero。在这个函数中,我们推迟‘调用一个匿名函数,该函数负责处理在执行DivideByZero时可能出现的任何死机。在这个延迟的匿名函数中,我们调用Recovery‘内置函数,并将其返回的错误赋给一个变量。如果DivideByZero死机,则设置该error值,否则为nil。通过比较err变量和nil,我们可以检测是否发生了死机,在本例中,我们使用log.Println函数记录死机,就像它是任何其他error一样。

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

在本例的输出中,我们首先看到来自恢复死机的匿名函数的日志消息,然后是消息我们幸存下来除以零!。我们确实做到了这一点,这要归功于恢复内置功能,它阻止了一场否则会终止我们围棋程序的灾难性恐慌。

Recover()返回的err值就是提供给Panic()调用的值。因此,确保在未发生死机时err值仅为零是至关重要的。

Recovery检测死机

Recovery‘函数根据错误的值来确定是否发生了死机。因为panic函数的参数是空接口,所以它可以是任何类型。任何接口类型的零值,包括空接口,都是nil。必须注意避免将nil作为panic`的参数,如下例所示:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6)
 7
 8func main() {
 9    divideByZero()
10    fmt.Println("we survived dividing by zero!")
11
12}
13
14func divideByZero() {
15    defer func() {
16    	if err := recover(); err != nil {
17    		log.Println("panic occurred:", err)
18    	}
19    }()
20    fmt.Println(divide(1, 0))
21}
22
23func divide(a, b int) int {
24    if b == 0 {
25    	panic(nil)
26    }
27    return a / b
28}

这将输出:

1[secondary_label Output]
2we survived dividing by zero!

此示例与上一个示例相同,只是稍微做了一些修改。修改了Divide函数,以检查其除数b是否等于0。如果是,它将使用带有参数nilpanic内置函数产生死机。这一次的输出不包括显示发生死机的日志消息,即使死机是由divide创建的。这种静默行为就是确保panic内置函数的参数不是nil非常重要的原因。

结论

我们已经看到了在围棋中制造恐慌的多种方式,以及如何通过使用内置的恢复来恢复恐慌。虽然你自己不一定会使用恐慌,但适当地从恐慌中恢复是让Go应用程序为生产做好准备的重要一步。

您还可以在Go series](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go).中探索[我们的整个如何编码

Published At
Categories with 技术
Tagged with
comments powered by Disqus