什么是panic?
在Go中,对于程序中出现的大部分异常情况,错误就已经够用了。但在有些情况下,当程序发生异常时无法继续运行,那么可使用panic
来终止程序。
当函数发生panic时,它会终止运行,在执行完所有延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出panic信息,接着打印出堆栈跟踪,最后程序终止。
何时应该使用panic?
通常,我们应该尽可能地使用错误,而不是使用panic和recover。只有当程序不能继续运行的时候,才使用panic和recover机制。
panic有两个合理的用例:
- 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是web服务器无法绑定所要求的端口。在这种情况下,就应该使用panic,因为如果不能绑定端口,接下来什么也做不了。
- 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用nil作为参数调用了它。在这种情况下,我们可以使用panic,因为这是一个编程错误:用nil参数调用了一个只能接收合法指针的方法。
示例
内建函数panic
的签名:func panic(v interface{})
,当程序终止时,会打印传入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")
}
上述代码,首先会打印出传入panic函数的信息:panic: runtime error: last name cannot be nil
,接着打印出堆栈跟踪。
发生panic时的defer
当函数发生panic时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出panic信息,接着打印出堆栈跟踪,最后程序终止。也就是说,如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。
使用一个例子来说明:
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
*/
recover
recover
是一个内建函数func recover() interface{}
,用于重新获得panic协程的控制。只有在延迟函数内部调用recover
才有用。在延迟函数内调用recover
,可以取到panic
的错误信息,并且停止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")
}
/*
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
*/
panic、recover、goroutine
只有在相同的Go协程中调用recover才管用。recover不能恢复一个不同协程的panic,看例子:
package main
import (
"fmt"
"time"
)
func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}
func a() {
defer recovery() // panic并不会恢复,因为是在b协程中发生panic
fmt.Println("Inside A")
go b()
time.Sleep(time.Second)
}
func b() {
fmt.Println("Inside B")
panic("oh! B panicked")
}
func main() {
a()
fmt.Println("normally returned from main")
}
/*
Inside A
Inside B
panic: oh! B panicked
*/
运行时panic
运行时错误(如数组越界)也会导致panic。这等价于调用了内置函数panic。
package main
import "fmt"
func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}
func a() {
defer recovery()
n := []int{3, 4, 5}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
/*
recovered: runtime error: index out of range
normally returned from main
*/
恢复后获得堆栈跟踪
当恢复panic时,就释放了它的堆栈跟踪。使用Debug
包中的PrintStack
函数可以打印出堆栈跟踪。
package main
import (
"fmt"
"runtime/debug"
)
func recovery() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
debug.PrintStack()
}
}
func a() {
defer recovery()
n := []int{3, 4, 5}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
reference: