panic和recover
panic和recover翻译成中文是恐慌和恢复,我个人感觉比较别扭,还是直接用英文吧。
什么是panic
panicGolang中处理错误的常规方式是使用前面我们讨论的error。但是有一些比较特殊的场景,发生错误后我们的程序就无法运行下去了。这时我们通过panic来终止程序的执行。当一个函数中发生panic时,该函数停止继续执行,然后该函数中被defer的函数执行,虽然程序执行控制权转移给外层函数。该过程会冒泡直到goroutine级别。然后打印panic信息和调用栈并终止整个程序的执行。现在看起来可能有点晦涩难懂,后面我们通过几个简单的例子看下应该会清晰很多。
如果发生了panic,我们可以通过recover来恢复程序的执行,panic和recover的关系就像其他编程语言中的try throw catch。
什么场景使用panic
panic我们通常应该使用error来处理异常情况,除非程序无法继续运行下去了再使用panic和recover。
以下两种情况比较适合使用panic:
有异常情况导致程序无法继续运行下去。
比如我们要实现web服务器,如果端口绑定失败,就可以生成一个
panic。编程错误。
比如我们期望一个函数的入参为指针,如果传进来的是
nil,那么就是编程实现逻辑错误,这时可以生成一个panic。
panic举例
panic举例Golang中panic函数的签名如下:
func panic(interfce{})传给panic函数的入参会在程序终止时打印出来。我们通过例子来理解下。
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")
}该程序用来打印一个人的姓名的全称。函数fullName中判断了firstName和lastName是否是nil。如果是nil就调用了panic方法,调用时传入了一个字符串用来表示错误信息。该错误信息会在程序终止时打印,该程序执行结果如下:
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0xc000074f68, 0x0)
/Users/goWorkspace/src/test/main.go:10 +0x145
main.main()
/Users/goWorkspace/src/test/main.go:18 +0x4d
exit status 2我们分析下这个打印结果来探究下panic的工作原理。我们调用fullName时第二个参数传的是nil,因此fullName内执行了panic语句。Golang程序执行过程中如果遇到panic就会终止执行,并打印传给panic的参数和调用栈。由于panic是发生在fullName函数内,因此一次打印了main.fullName(0xc000074f68, 0x0)和main.main()。
defer + panic
defer + panic如果一个函数内发生了panic,那么会先执行defer调用的函数,然后再转移到外层函数。该过程会不断向外冒泡直到goroutine级别。然后打印panic信息和调用栈。 前面的例子中没有defer函数调用。如果有defer函数调用,那么会先执行defer调用的函数,然后程序执行控制权再转移到外层函数。下面我们增加个defer调用语句。
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")
}该程序执行结果如下:
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0xc000074f58, 0x0)
/Users/work/goWorkspace/src/test/main.go:11 +0x1d1
main.main()
/Users/work/goWorkspace/src/test/main.go:20 +0xa5
exit status 2这段程序中fullName发生panic时先执行被defer的函数调用,打印了deferred call in fullName。然后程序执行控制权转移给外层函数,接着执行外层函数被defer的函数调用,打印了deferred call in main。此时已到goroutine级别,因此不会再接着向外冒泡了。接着打印了panic信息以及调用栈。
recover
recoverrecover跟panic一样,也是Golan内置函数。recover一般在被defer的函数内部调用,用来将程序恢复到正常。panic和recover跟其他编程需要中的try catch非常像,一个用来抛错误,一个用来处理错误并恢复程序的正常执行。recover函数签名如下:
func recover() interface{}下面我们通过例子看下recover是如何将panic的程序恢复到正常执行的。
package main
import "fmt"
func recoverName() {
if r := recover(); r != nil {
fmt.Println("recovered from ", r)
}
}
func fullName(firstName *string, lastName *string) {
defer recoverName()
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")
}这里我们在被defer的函数中调用了recover来将panic恢复到正常。注意recover()函数的调用结果即为panic的参数。这里我们拿到panic的参数后只是进行了打印。该程序执行结果如下:
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in mainrecover只能恢复当前goroutine的panic
recover只能恢复当前goroutine的panicrecover只能恢复当前goroutine的panic,不能恢复其他gorutine的panic。
package main
import (
"fmt"
"time"
)
func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}
func a() {
defer recovery()
fmt.Println("Inside A")
go b()
time.Sleep(1 * time.Second)
}
func b() {
fmt.Println("Inside B")
panic("oh! B panicked")
}
func main() {
a()
fmt.Println("normally returned from main")
}这里在goroutine b中发生了panic,但是函数a的执行位于主goroutine,跟b不是一个,因此函数a中的recover无法恢复b中的panic。该程序执行结果如下:
Inside A
Inside B
panic: oh! B panicked
goroutine 5 [running]:
main.b()
/Users/work/goWorkspace/src/test/main.go:23 +0x79
created by main.a
/Users/work/goWorkspace/src/test/main.go:17 +0x95
exit status 2如果b函数调用不是在一个新的goroutine,比如我们将go b()改为b(),则执行结果如下:
Inside A
Inside B
recovered: oh! B panicked
normally returned from main这时b和a的执行都位于主goroutine,因此可以正常恢复。
运行时panic
panic在Golang中,有一些运行时错误也会引发panic,比如数组访问越界。这种情况相当于我们手动调用panic,并传入runtime.Error类型的参数。接口runtime.Error定义如下:
type Error interface {
error
// RuntimeError is a no-op function but
// serves to distinguish types that are run time
// errors from ordinary errors: a type is a
// run time error if it has a RuntimeError method.
RuntimeError()
}从其定义能够看出来,runtime.Error接口也实现了error接口。我们通过例子来看下运行时panic。
package main
import "fmt"
func a() {
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}这里我们访问切片n的索引为3的位置,显然越界了。因此引发运行时panic。该程序输出如下:
panic: runtime error: index out of range
goroutine 1 [running]:
main.a()
/Users/work/goWorkspace/src/test/main.go:7 +0x11
main.main()
/Users/work/goWorkspace/src/test/main.go:11 +0x22
exit status 2运行时panic也可以通过recover来恢复。
package main
import "fmt"
func r() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}
func a() {
defer r()
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}这里我们在被defer的函数r中将panic的程序恢复正常。该程序输出为:
Recovered runtime error: index out of range
normally returned from mainrecover后打印调用栈
recover后打印调用栈从前面的几个例子我们会发现,程序recover后调用栈就不会被自动打印了。不过我们可以借助Debug包的PrintStack函数来手动打印调用栈。
package main
import (
"fmt"
"runtime/debug"
)
func r() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
debug.PrintStack()
}
}
func a() {
defer r()
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}这里我们通过debug.PrintStack()来手动打印了调用栈。该程序输出如下:
Recovered runtime error: index out of range
goroutine 1 [running]:
runtime/debug.Stack(0xc00007a008, 0xc00006ee08, 0x2)
/usr/local/go/src/runtime/debug/stack.go:24 +0xa7
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.r()
/Users/work/goWorkspace/src/test/main.go:11 +0x9c
panic(0x10a9bc0, 0x115d560)
/usr/local/go/src/runtime/panic.go:513 +0x1b9
main.a()
/Users/work/goWorkspace/src/test/main.go:18 +0x3e
main.main()
/Users/work/goWorkspace/src/test/main.go:23 +0x22
normally returned from main这里我们先恢复了程序的正常,然后紧接着手动打印了调用栈。
Last updated
Was this helpful?