Go:Panic与Recover

1.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 := "SS"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

运行输出:

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

从例子中可以看到,当给firstName分配值并未给lastName分配,调用fullName,当执行到 lastName == nil 时,程序会被终止,传递给panic函数的参数被打印出来,然后是堆栈信息 。由于程序在执行 panic 函数调用之后终止,后面代码将不再执行。

所以该程序首先打印传递给panic函数的消息

1
panic: runtime error: last name cannot be nil

然后打印堆栈信息,根据堆栈跟踪,一项一项的打印。

2.例2

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

例子中试图访问切片中n[4]的无效索引,则该程序将有以下输出:

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

3.在Panic中使用defer

当一个函数遇到panic时,它的执行会停止,但是任何defer的函数都会被执行,然后控制权返回给它的调用者。这个过程一直持续到当前 goroutine 的所有函数都已经返回,此时程序会打印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
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 := "SS"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

上例唯一更改是添加了defer函数调用,现在的输出为:

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

当程序遇到panic时,首先执行defer的函数调用,然后控制返回到执行defer调用的调用者,依此类推。 最后是堆栈跟踪信息。

4.panic中使用recover

仅当在defer函数内部调用时,recover才有用。在defer函数内执行recover恢复正常执行来停止panic,并检索传递给panic函数调用的错误消息。如果在defer函数之外调用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 := "SS"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

调用recover()返回传递给panic函数调用的值, 当fullName函数调用时,recoverFullName()将调用defer函数,用于recover()停止panic,打印结果

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

当程序在 lastName == nil 发生panic时。recoverFullName函数被调用,该函数又调用recover()以重新获得对panic的控制。并将返回值传递给的panic(),因此它打印:

1
recovered from  runtime error: last name cannot be nil  

执行后recover(),panic停止,控制权返回给调用者,在这种情况下,是main函数。程序从 fmt.Println(“returned normally from main”) 开始继续正常执行,main的panic已经recover了。它打印returned normally from main 后跟deferred call in main

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

5.recover后获取stack跟踪

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 := "SS"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

在上面的例子中,使用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打印出来了之后,打印堆栈跟踪。然后在panic恢复后打印

1
2
returned normally from main  
deferred call in main

6.panic、recover和Goroutines

Recover 仅在从恐慌的同一个 goroutine 中调用时才起作用。无法从不同 goroutine 中发生的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 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()将发生panic。因为 b 是零并且零不能做除数。该sum()函数调用一个defer函数recovery(),用于从panic中恢复。该函数divide()作为单独的 goroutine 调用,在done channel等待,确保divide()完成并执行。

该例中panic将无法recover。这是因为该recovery函数存在于不同的 goroutine 中,而panic发生在divide()不同 goroutine中的函数中。因此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

可以从输出中看出panic并未recover。 如果divide()在同一个 goroutine 中调用该函数,就会从panic中recover过来

如果将上面例子中 go divide(a, b, done) 改为 divide(a, b, done) ,panic将会被recover,因为panic发生在同一个 goroutine 中,则它将打印:

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