- 原文地址:Part 32: Panic and Recover
- 原文做者:Naveen R
- 译者:咔叽咔叽 转载请注明出处。
处理Go中异常状况的惯用方法是使用errors,对于程序中出现的大多数异常状况,errors就足够了。golang
可是在某些状况下程序不能在异常状况下继续正常执行。在这种状况下,咱们使用panic来终止程序。函数遇到panic时将会中止执行,若是有defer的话就执行defer延迟函数,而后返回其调用者。此过程一直持续到当前goroutine的全部函数都返回,而后打印出panic信息,而后是堆栈信息,而后程序终止。待会儿用一个例子来解释,这个概念就会更加清晰一些了。数组
咱们可使用recover函数恢复被panic终止的程序,将在本教程后面讨论。bash
panic和recover有点相似于其余语言中的try-catch-finally语句,可是前者使用的比较少,并且使用时更优雅代码也更简洁。服务器
通常状况下咱们应该避免使用panic和recover,尽量使用errors。只有在程序没法继续执行的状况下才应该使用panic和recover。ide
不可恢复的错误,让程序不能继续进行。 好比说Web服务器没法绑定到指定端口。在这种状况下,panic是合理的,由于若是端口绑定失败接下来的逻辑继续也是没有意义的。函数
coder的人为错误 假设咱们有一个接受指针做为参数的方法,然而使用了nil做为参数调用此方法。在这种状况下,咱们能够用panic,由于该方法须要一个有效的指针。ui
panic函数的定义spa
func panic(interface{}) 复制代码
当程序终止时,参数会传递给panic函数打印出来。看看下面例子的panic是如何使用的。debug
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(0x1040c128, 0x0)
/tmp/sandbox135038844/main.go:12 +0x120
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
复制代码
咱们来分析一下这个输出,来了解panic是如何工做以及如何打印堆栈跟踪的。 在第19行,咱们将Elon定义给firstName。而后调用fullName函数,其中lastName参数为nil。所以,第11行将触发panic。当触发panic时,程序执行就终止了,而后打印传递给panic的内容,最后打印堆栈跟踪信息。所以14行之后的代码不会被执行。 该程序首先打印传递给panic函数的内容,
panic: runtime error: last name cannot be nil
复制代码
而后打印堆栈跟踪信息。 该程序在12行触发panic,所以,
ain.fullName(0x1040c128, 0x0)
/tmp/sandbox135038844/main.go:12 +0x120
复制代码
将被首先打印。而后将打印堆栈中的下一个内容,
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
复制代码
如今已经返回到了形成panic的顶层main函数,所以打印结束。
咱们回想一下panic的做用。当函数遇到panic时,将会终止panic后面代码的执行,若是函数体包含有defer函数的话会执行完defer函数。而后返回其调用者。此过程一直持续到当前goroutine的全部函数都返回,此时程序打印出panic内容,而后是堆栈跟踪信息,而后终止。
在上面的示例中,咱们没有任何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")
}
复制代码
Run in playground 对以前代码所作的惟一更改是在fullName函数和main函数中第一行添加了defer函数调用。 运行的输出,
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1042bf90, 0x0)
/tmp/sandbox060731990/main.go:13 +0x280
main.main()
/tmp/sandbox060731990/main.go:22 +0xc0
复制代码
当发生panic时,首先执行defer函数,而后到下一个defer调用,依此类推,直到达到顶层调用者。
在咱们的例子中,defer声明在fullName函数的第一行。首先执行fullName函数。打印
deferred call in fullName
复制代码
而后调用返回到main函数的defer,
deferred call in main
复制代码
如今调用已返回到顶层函数,而后程序打印panic内容,而后是堆栈跟踪信息,而后终止。
recover是一个内置函数,用于goroutine从panic的中断情况中恢复。 函数定义以下,
func recover() interface{}
复制代码
recover只有在defer函数内部调用时才有效。defer函数内经过调用recover可让panic中断的程序恢复正常执行,调用recover会返回panic的内容。若是在defer函数以外调用recover,它将不会中止panic序列。
修改一下,使用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")
}
复制代码
Run in playground 第7行调用了recoverName函数。这里打印了recover返回的值, 发现recover返回的是panic的内容。
打印以下,
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
复制代码
程序在19行触发panic,defer函数recoverName经过调用recover来从新控制该goroutine,
recovered from runtime error: last name cannot be nil
复制代码
在执行recover以后,panic中止而且返回到调用者,main函数和程序在触发panic以后将继续从第29行执行。而后打印,
returned normally from main
deferred call in main
复制代码
recover仅在从同一个goroutine调用时才起做用。从不一样的goroutine触发的panic中recover是不可能的。再来一个例子来加深理解。
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")
}
复制代码
Run in playground 在上面的程序中,函数b在23行触发panic。函数a调用defer函数recovery用于从panic中恢复。函数a的17行用另一个goroutine执行b函数。Sleep的做用只是为了确保程序在b运行完毕以前不会被终止,固然也能够用sync.WaitGroup来解决。
你认为该段代码的输出是什么?panic会被恢复吗?答案是不能够。panic将没法被恢复。这是由于recover存在于不一样的gouroutine中,而且触发panic发生在不一样goroutine执行的b函数。所以没法恢复。 运行的输出,
Inside A
Inside B
panic: oh! B panicked
goroutine 5 [running]:
main.b()
/tmp/sandbox388039916/main.go:23 +0x80
created by main.a
/tmp/sandbox388039916/main.go:17 +0xc0
复制代码
能够从输出中看到恢复失败了。
若是在同一个goroutine中调用函数b,那么panic就会被恢复。 在第17行把, go b()
换成 b()
那么会输出,
Inside A
Inside B
recovered: oh! B panicked
normally returned from main
复制代码
panic还可能由运行时的错误引发,例如数组越界访问。这至关于使用由接口类型runtime.Error定义的参数调用内置函数panic。 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")
}
复制代码
Run in playground 在上面的程序中,第9行咱们试图访问n [3],这是切片中的无效索引。这个会触发panic,输出以下,
panic: runtime error: index out of range
goroutine 1 [running]:
main.a()
/tmp/sandbox780439659/main.go:9 +0x40
main.main()
/tmp/sandbox780439659/main.go:13 +0x20
复制代码
您可能想知道是否运行中的panic可以被恢复。答案是确定的。让咱们修改上面的程序,让panic恢复过来。
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")
}
复制代码
Run in playground 执行后输出,
Recovered runtime error: index out of range
normally returned from main
复制代码
显然能够看到panic被恢复了。
咱们恢复了panic,可是丢失了此次panic的堆栈调用的信息。 有一种方法能够解决这个,就是使用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")
}
复制代码
Run in playground 在11行调用了debug.PrintStack,能够看到随后输出,
Recovered runtime error: index out of range
goroutine 1 [running]:
runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)
/usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()
/tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)
/usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()
/tmp/sandbox949178097/main.go:18 +0x80
main.main()
/tmp/sandbox949178097/main.go:23 +0x20
normally returned from main
复制代码
从输出中能够知道,首先是panic被恢复而后打印Recovered runtime error: index out of range
,再而后打印堆栈跟踪信息。最后在panic被恢复后打印normally returned from main