Panic和Recover
Q7nl1s admin

什么是Panic?

在Go程序中,处理异常情况的惯用方法是使用errors。对于程序中出现的大多数异常情况,errors已经足够了。

但有些情况下,程序在出现异常情况后无法继续执行。在这种情况下,我们使用panic来过早地终止程序。当一个函数遇到panic时,它的执行被停止,任何defer函数被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回为止,这时程序会打印出panic信息,然后是堆栈跟踪,然后终止。当我们写一个例子程序时,这个概念就会更清楚。

使用recover可以重新获得对panic程序的控制,我们将在本教程的后面讨论。

panic和recover可以说是类似于其他语言(如Java)中的try-catch-finally语法,只是在Go中很少使用。

什么时候应该使用panic?

一个重要的因素是,你应该避免panicrecover,尽可能地使用errors。只有在程序无法继续执行的情况下,才应该使用panic和recover机制。

panic有两种有效的使用情况。

  1. 一个无法恢复的错误,程序不能简单地继续执行。
    一个例子是一个网络服务器无法绑定到所需的端口。在这种情况下,panic是合理的,因为如果端口绑定本身失败了,就没有其他事情可做。
  2. 一个程序员的错误。
    假设我们有一个接受指针作为参数的方法,有人用一个nil参数调用这个方法。在这种情况下,我们可以panic,因为用nil参数调用一个方法是一个程序员的错误,而这个方法期望的是一个有效的指针。

panic实例

下面提供了内置panic函数的签名。

1
func panic(interface{})  

当程序终止时,传递给panic函数的参数将被打印出来。当我们写一个示例程序时,它的用途就会很清楚。所以我们马上就来做这件事。

我们将从一个假想的例子开始,它展示了panic是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

func fullName(firstName *string, lastName *string) {
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

在操场上运行

以上是一个简单的程序,用于打印一个人的全名。第7行中的fullName函数可以打印一个人的全名。该函数在第8行和第11行分别检查firstNamelastName指针是否为nil。如果为nil,该函数将调用panic,并给出相应的信息。这个信息将在程序终止时被打印出来。

运行这个程序将打印出以下输出。

1
2
3
4
5
6
7
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193
main.main()
/tmp/sandbox210590465/prog.go:20 +0x4d

让我们分析一下这个输出,以了解panic是如何工作的,以及当程序发生panic时,堆栈跟踪是如何打印的。

在第19行,我们将Elon分配给firstName。在第20行中,我们调用fullName函数,而lastNamenil。因此,第11行的条件满足,程序将出现panic。当遇到panic时,程序执行终止,传递给panic函数的参数被打印出来,然后是堆栈跟踪。由于程序在第12行调用panic函数后终止,第13、14和15行的代码将不会被执行。

这个程序首先打印了传递给panic函数的信息。

1
panic: runtime error: last name cannot be nil  

然后打印出堆栈跟踪。

程序在fullName函数的第12行慌了手脚,因此

1
2
3
goroutine 1 [running]:  
main.fullName(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193

将首先被打印出来。然后,堆栈中的下一个项目将被打印出来。在我们的例子中,main函数第20行调用fullName的是堆栈跟踪中的下一个项目。因此,它将被打印出来。

1
2
main.main()  
/tmp/sandbox210590465/prog.go:20 +0x4d

现在我们已经到达了引起panic的顶层函数,上面已经没有更多的层次了,因此没有什么可打印的了。

再举一个例子

panic也可以由运行时发生的错误引起,比如试图访问一个不存在于切片中的索引。

让我们写一个特意设计的例子,由于超界的切片访问而产生panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func slicePanic() {
n := []int{5, 7, 4}
fmt.Println(n[4])
fmt.Println("normally returned from a")
}
func main() {
slicePanic()
fmt.Println("normally returned from main")
}

在操场上运行

在上面的程序中,在第9行,我们试图访问n[4],而这是切片中的无效索引。这个程序会出现panic,输出结果如下:

1
2
3
4
5
6
7
panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.slicePanic()
/tmp/sandbox942516049/prog.go:9 +0x1d
main.main()
/tmp/sandbox942516049/prog.go:13 +0x22

panic期间的defer调用

让我们回忆一下panic的作用。当一个函数遇到panic时,它的执行被停止,任何延迟的函数被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回为止,这时程序会打印出panic信息,然后是堆栈跟踪,然后终止。

在上面的例子中,我们没有推迟任何函数调用。如果有一个defer函数调用,它就会被执行,然后控制权再返回给它的调用者。

让我们稍微修改一下上面的例子,使用一个defer语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

func fullName(firstName *string, lastName *string) {
defer fmt.Println("deferred call in fullName")
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

在操场上运行

唯一的变化是在第8行和第20行增加了延迟的函数调用。

这个程序会打印出来。

1
2
3
4
5
6
7
8
9
deferred call in fullName  
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af28, 0x0)
/tmp/sandbox451943841/prog.go:13 +0x23f
main.main()
/tmp/sandbox451943841/prog.go:22 +0xc6

当程序在第13行出现panic时,任何defer函数调用都会首先被执行,然后控制权返回其上层调用者,以此类推,直到到达最高级别的调用者。

在我们的例子中,fullName函数第8行中的defer语句首先被执行,这将打印出以下信息:

1
deferred call in fullName  

然后控制权返回到main函数,main函数的延迟调用被执行,因此会打印出以下信息:

1
deferred call in main  

现在控制权已经到达顶层函数,因此程序会打印出panic信息和堆栈跟踪,然后终止。

从panic中recover

recover是一个内置函数,用于重新获得对panic程序的控制。

以下是recover函数的签名。

1
func recover() interface{}  

recover函数只有在延迟函数内调用时才有用。在defer函数内部执行对recover的调用,通过恢复正常执行来停止panic序列,并检索传递给panic函数的errors信息。如果recover在defer函数之外被调用,它将不会停止panic序列。

让我们修改我们的程序,使用recover来恢复panic后的正常执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
)

func recoverFullName() {
if r := recover(); r!= nil {
fmt.Println("recovered from ", r)
}
}

func fullName(firstName *string, lastName *string) {
defer recoverFullName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

在操场上运行

第7行的recoverFullName()函数调用recover(),该函数返回传递给panic函数的值。这里我们只是在第9行打印recover()返回的值。recoverFullName()fullName函数的第14行中被推迟了。

fullName发生panic时,defer函数recoverName()将被调用,它使用recover()来停止panic。

这个程序会打印出来。

1
2
3
recovered from  runtime error: last name cannot be nil  
returned normally from main
deferred call in main

当程序在第19行panic时,延迟的recoverFullName()将被调用,该函数反过来调用recover()以重新获得对panic的控制。第8行对recover()的调用将返回传递给panic的参数,因此它打印出来。

1
recovered from  runtime error: last name cannot be nil  

在执行recover()后,panic停止,控制权返回到上级调用者,在这里是指main函数。由于panic已经被恢复了,程序从main的第29行开始继续正常执行😃。它打印先打印returned normally from main再打印deferred call in main

让我们再看一个例子,我们从访问切片的无效索引引起的panic中恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func recoverInvalidAccess() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}

func invalidSliceAccess() {
defer recoverInvalidAccess()
n := []int{5, 7, 4}
fmt.Println(n[4])
fmt.Println("normally returned from a")
}

func main() {
invalidSliceAccess()
fmt.Println("normally returned from main")
}

在操场上运行

运行上述程序将输出。

1
2
Recovered runtime error: index out of range [4] with length 3  
normally returned from main

从输出结果来看,你可以了解到我们已经从panic中恢复过来了。

recover后获取堆栈跟踪

如果我们从panic中恢复,我们会失去关于panic的堆栈跟踪。甚至在上面的程序恢复后,我们也失去了堆栈跟踪。

有一种方法可以使用Debug包的PrintStack函数来打印堆栈记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"runtime/debug"
)

func recoverFullName() {
if r := recover(); r != nil {
fmt.Println("recovered from ", r)
debug.PrintStack()
}
}

func fullName(firstName *string, lastName *string) {
defer recoverFullName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

在操场上运行

在上面的程序中,我们在第11行使用debug.PrintStack()来打印堆栈跟踪。

这个程序将打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
recovered from  runtime error: last name cannot be nil  
goroutine 1 [running]:
runtime/debug.Stack(0x37, 0x0, 0x0)
/usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
/usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x22
main.recoverFullName()
/tmp/sandbox771195810/prog.go:11 +0xb4
panic(0x4a1b60, 0x4dc300)
/usr/local/go-faketime/src/runtime/panic.go:969 +0x166
main.fullName(0xc0000a2f28, 0x0)
/tmp/sandbox771195810/prog.go:21 +0x1cb
main.main()
/tmp/sandbox771195810/prog.go:30 +0xc6
returned normally from main
deferred call in main

从输出中,你可以了解到,panic被恢复了,并且recovered from runtime error: last name cannot be nil被打印出来。在这之后,堆栈跟踪被打印出来。然后

1
2
returned normally from main  
deferred call in main

是在panic恢复后打印的。

panic、recover和Goroutines

只有当它从发生panic的同一个goroutine中调用时,recover才起作用。不可能从发生在不同goroutine的panic中recover。让我们通过一个例子来理解这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
)

func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}

func sum(a int, b int) {
defer recovery()
fmt.Printf("%d + %d = %d\n", a, b, a+b)
done := make(chan bool)
go divide(a, b, done)
<-done
}

func divide(a int, b int, done chan bool) {
fmt.Printf("%d / %d = %d", a, b, a/b)
done <- true

}

func main() {
sum(5, 0)
fmt.Println("normally returned from main")
}

在操场上运行

在上面的程序中,函数divide()将在第22行发生panic,因为b是0,而一个数字不可能被0除。sum()函数调用了一个延迟函数recovery(),用于从panic恢复。函数divide()在第17行作为一个单独的goroutine被调用。我们在第18行中等待已完成的通道,以确保divide()完成执行。

你认为该程序的输出是什么?panic是否会被恢复?答案是否定的。panic将不会被恢复。这是因为recover函数存在于不同的goroutine中,而panic发生在不同goroutine的divide()函数中。因此,recover是不可能的。

运行这个程序将打印。

1
2
3
4
5
6
7
8
5 + 0 = 5  
panic: runtime error: integer divide by zero

goroutine 18 [running]:
main.divide(0x5, 0x0, 0xc0000a2000)
/tmp/sandbox877118715/prog.go:22 +0x167
created by main.sum
/tmp/sandbox877118715/prog.go:17 +0x1a9

你可以从输出中看到,recover并没有发生。

如果在同一个goroutine中调用divide()函数,我们就会从panic中恢复。

如果将程序的第17行的

1
go divide(a, b, done)  

改为

1
divide(a, b, done)  

就会发生recover,因为panic发生在同一个goroutine中。如果程序运行时有上述改变,它将打印出

1
2
3
5 + 0 = 5  
recovered: runtime error: integer divide by zero
normally returned from main

这样我们就到了本教程的结尾。

下面是对本教程中所学内容的简要回顾。

  • 什么是Panic?
  • 什么时候应该使用Panic?
  • panic的使用实例
  • panic期间defer函数
  • 从panic中recover
  • recover后获得堆栈跟踪
  • panic、recover和Goroutines
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View