Go教程第三十篇:故障及恢复
panic-and-recover
本文是《Go系列教程》的第三十篇文章。
什么是panic ?
Go程序处理异常条件的惯用方式是使用error。对于在程序中引起的大多数异常条件,error都能够满足要求。但是有些情况下,当程序发生异常情况的时候,程序会无法继续运行。在这种情况下,我们可以使用panic来提前终止程序。当函数发生panic之后,它的执行就会停止。任何延迟执行的函数都会被执行,之后程序的控制逻辑返回到它的调用者。此过程会一直持续到当前goroutine的所有函数都已返回。我们下面会写一个示例程序,帮助大家理解。
在后面我们会讨论,使用recover也能重新获得对故障程序的控制。
Go中的故障恢复和其他语言中的try-catch-finally的语义类似。像java异常这样的,在Go中几乎很少使用。
什么时候使用panic?
有一个重要的要素是,你应该避免panic和recover,并尽可能地使用error来处理异常情况。只有当程序无法继续运行的时候,我们才能使用panic-recover机制。
这里有俩个有效的使用场景:
1、发生了无法自动恢复的错误,此时程序不能继续往下运行。
举个例子,一个web服务器,无法绑定到所需的端口,在这种情况下,就应该使用panic。因为如果端口绑定失败的话,程序就不能执行任何操作。
2、编程错误
我们说,我们有一个方法接收一个指针类型的参数,而某个人调用此方法时,传递了一个nil参数。在这种情况下,我们就可以使用panic,因为这是一个编程错误,方法要求的是一个有效的指针值参数,而传递的确是一个nil值。
panic示例一
内置的panic函数的签名如下:
func panic(interface{})
当程序结束的时候,会把传递给panic函数的参数打印出来。 我们如果写一个示例程序的话,这种用法就会显得清晰易懂,接下来我们就马上写个程序试试吧。
我们写一个人为的示例,用以展示一下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(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193
main.main()
/tmp/sandbox210590465/prog.go:20 +0x4d
我们来分析一下这个输出,以便于理解panic是如何工作的,以及当程序panic的时候,如何打印的错误堆栈信息。
在19行,我们把Elon赋值给了firstName。之后,我们调用了fullName()函数,传递的参数分别为:“Elon”和nil。
因此,程序将发生panic。当程序发生panic时,程序会结束运行,在打印堆栈之后,会紧接着把传递给panic的参数打印出来。由于程序在panic函数调用之后,就结束运行了,因此后面的代码也就得不到运行的机会。
程序会首先打印传递给panic函数的消息。
panic: runtime error: last name cannot be nil
之后,打印堆栈信息。
在fullName的第12行,程序发生panic,故而先打印:
goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193
之后,打印堆栈中的下一个异常信息,因此打印输出为:
main.main()
/tmp/sandbox210590465/prog.go:20 +0x4d
到此为止,我们就到达了导致此panic的顶层函数,其上层在没有其他信息,因此,也就没有别的输出了。
panic示例二
在运行期间发生的error也会导致panic,例如,试图访问一个数组中不存在的角标等。
我们来写一个人为的示例,展示一下越界访问也会导致panic。
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],而角标4却压根在数组中不存在。程序此时会发生panic,并
产生如下输出:
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语句。
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行增加了延迟函数调用。程序的打印如下:
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的时候,任何的延迟函数调用都会被首先执行,之后控制逻辑返回到延迟函数的调用者手中。
在我们的例子中,首先执行fullName的defer语句,它会打印下面这些信息。
deferred call in fullName
之后,控制逻辑会返回到main函数手中,故而此时会执行main函数中的延迟调用,因此打印输出如下:
deferred call in main
现在控制逻辑已经到达了顶层函数,因此程序会打印panic消息,紧接着的是异常堆栈,之后,程序会结束运行。
从panic中恢复
recover是一个内置函数,它可用于重获对panic程序的控制。
recover函数的签名如下:
func recover() interface{}
recover只有在延迟函数内部调用的时候才有用。在延迟函数内部执行recover调用, 会停止后续的panic,恢复正常的调用,并获取传递给panic函数的错误信息。如果recover在延迟函数外部调用的话,它不会停止后续的panic。
我们对上面的程序进行修改,并在发生panic之后,使用recover来恢复正常运行。
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")
}
第七行的recoverFullName()函数调用了recover函数,recover函数会返回传递给panic函数的返回值。因此,我们仅打印出了recover函数返回的值。recoverFullName()又在fullName函数的第14行被延迟调用。
当fullName函数发生panic的时候,延迟函数recoverName()将会被调用,而recoverName()却又会使用recover来停止后续的panic。
程序将输出如下:
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
当程序在第19行发生panic时,延迟函数recoverFullName会被调用,而recoverFullName又会调用recover(),recover会重获控制。
recover()调用会返回传递给panic函数的参数,因此它会打印:
recovered from runtime error: last name cannot be nil
在recover执行完之后,就会停止panic,控制逻辑返回到main函数手中。从29行之后,程序会继续正常执行,因为panic已经被恢复了。它打印了“returned normally from main”,
紧接着又打印出了"deferred call in main"。
我们再来看一个例子,在这个例子中我们会使用recover从由于访问数组无效角标的导致的panic中恢复。
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")
}
运行上面的程序,会得到如下输出:
Recovered runtime error: index out of range [4] with length 3
normally returned from main
从上面的输出中,你就会看到,我们已经从panic中恢复啦。
故障恢复之后,获取堆栈信息
如果我们从panic中恢复,我们就丢失了此panic的异常堆栈。甚至在上面的程序恢复之后,我们丢失了堆栈。有一种方式可以打印堆栈信息,那就是使用Debug包下的PrintStack函数。
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")
}
在上面的程序中,我们使用debug.PrintStack()来打印堆栈信息。程序的输出如下:
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恢复之后,打印出了:
returned normally from main
deferred call in main
Panic,Recover 以及Goroutine
只有当recover和panic在同一个goroutine中时,recover才可以正常工作。如果发生panic的goroutine和recover()函数所在的goroutine不是同一个的话,也就无法恢复。我们写段示例来理解一下。
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()函数调用了一个延迟函数recovery(),该函数专门用于从panic中恢复。函数divide()作为一个单独的goroutine被调用。我们在done通道上等待,以确保divide执行完成。
你可以想象一下,程序的输出是什么。panic可以被恢复吗? 答案是否。panic无法被恢复。这是因为recovery函数位于不同的goroutine中。而panic发生在另一个goroutine的divide()函数中,因此,无法从panic恢复。
运行此程序将得到如下输出:
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
从输出中,你可以看到恢复并没有发生。如果divide()函数是在同一个goroutine中被调用的话,我们就可以从panic中恢复过来。
在程序的17行,我们把程序由:
go divide(a, b, done)
修改为:
divide(a, b, done)
此时,恢复就可以正常运行,因为panic是在同一个goroutine中发生的。把程序做如上修改,运行之后,它将输出如下:
5 + 0 = 5
recovered: runtime error: integer divide by zero
normally returned from main
备注
本文系翻译之作原文博客地址