@phper
2018-03-13T10:08:32.000000Z
字数 7576
阅读 4199
Golang
原文:https://golangbot.com/panic-and-recover/

欢迎访问Golang 系列教程中的第32章。也是本系列的最后一章。
在 go 程序中处理异常情况的惯用方法是使用错误(errors)。对于程序中出现的大多数异常情况, 使用错误都是足够的。
但在某些情况下,程序不能在异常情况之后继续执行。在这种情况下, 我们可以使用panic来终止程序。当一个函数(function)遇到恐慌(panic)时,它的执行被停止,任何延迟(defer)的函数都会被执行,然后控制权返回给调用者。这个过程继续下去,直到当前goroutine的所有函数都返回到程序打印出恐慌消息的地方,然后是堆栈跟踪,然后终止。当我们编写一个示例程序时,这个概念会更加清晰。
可以使用recover重新获得对恐慌程序的控制, 我们将在本教程后面讨论这些问题。
恐慌和恢复和其他编程语言中的try-catch-finally语法相似,除了它很少使用之前,当然它在被使用时会更优雅,并且代码整洁干净。
一个重要的因素是, 您应该避免惊慌和恢复, 并尽可能使用错误。只有在程序无法继续执行的情况下, 才应使用恐慌和恢复机制。
有两种有效的恐慌使用案例。
一个无法恢复的错误, 程序不能简单地继续执行它。
一个示例是 web 服务器无法绑定到所需的端口。在这种情况下, 如果端口绑定本身失败, 那么就有理由恐慌。
程序员错误。
假设,我们有一个接受指针作为参数的方法,并有人使用nil参数来调用此方法。在这种情况下,我们可能会因程序员的错误而惊慌失措,以nil期望有效指针的参数调用方法。
内置panic函数的语法如下,
func panic(interface{})
当程序终止时, 传递给恐慌的参数将被打印出来。当我们编写一个示例程序时,这个用法就很清楚了。所以, 让我们马上做。
我们将从一个人为例子开始,来说明恐慌如何起作用。
package mainimport ("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")}
上面是一个简单的程序,用来打印一个人的全名。第7行中的函数fullName用来打印一个人的全名。在第8和11行这个函数会检查 "名字" 和 "姓氏" 指针是否分别为nil。如果它是nil, 则该函数将使用相应的错误消息来调用panic。此错误消息将在程序终止时打印。
运行此程序将打印以下输出,
panic: runtime error: last name cannot be nilgoroutine 1 [running]:main.fullName(0x1040c128, 0x0)/tmp/sandbox135038844/main.go:12 +0x120main.main()/tmp/sandbox135038844/main.go:20 +0x80
让我们分析一下这个输出, 来了解panic是如何工作的, 以及当程序异常时如何打印堆栈跟踪。
在第19行中, 我们将Elon分配给了firstName。在20行中, 我们调用lastName为nil的fullName函数。因此, 第11 行条件成立, 程序将恐慌。遇到恐慌时, 程序执行终止, 打印传递给panic的参数, 然后输出堆栈跟踪。因此, 在恐慌之后, 将不会执行14和15行中的代码。这个程序首先打印传递给panic函数的消息,
panic: runtime error: last name cannot be empty
然后打印堆栈跟踪。
程序在fullName函数12行中输出panic, 因此,
main.fullName(0x1040c128, 0x0)/tmp/sandbox135038844/main.go:12 +0x120
将首先打印。然后将打印堆栈中的下一项。在我们的案例中, 20 行是堆栈跟踪中的下一个项目, 它是导致此行发生恐慌的fullName调用, 因此
main.main()/tmp/sandbox135038844/main.go:20 +0x80
会接着打印。现在我们已经达到了导致恐慌的顶层功能, 并且上面没有更多的级别, 因此没有更多的打印。
让我们回想一下恐慌是怎么回事。当某个函数遇到恐慌(panic)时, 它的执行被停止, 任何推迟(defer)的函数都会被执行, 然后控制权返回给调用者。此过程将继续, 直到当前 goroutine 的所有函数都返回时, 程序打印出紧急消息,随后是堆栈跟踪,然后终止。
在上面的例子中, 我们没有推迟任何函数调用。如果存在延迟(defer)函数调用, 则执行该函数,然后控件返回给调用者。
让我们稍微修改一下上面的示例, 然后使用延迟语句。
package mainimport ("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 fullNamedeferred call in mainpanic: runtime error: last name cannot be nilgoroutine 1 [running]:main.fullName(0x1042bf90, 0x0)/tmp/sandbox060731990/main.go:13 +0x280main.main()/tmp/sandbox060731990/main.go:22 +0xc0
当程序在13行中死机时, 将首先执行任何延迟的函数调用, 然后控制返回到执行延迟调用的调用者, 直到到达顶级调用方为止。
在我们的案例中,defer语句在fullName函数的第8行中首先执行。此打印
deferred call in fullName
然后控制返回到main函数, 其延迟调用被执行, 因此打印,
deferred call in main
现在控制器已经达到了最顶层,因此程序打印出恐慌消息,之后是堆栈跟踪消息,然后终止。
panic: runtime error: last name cannot be nil
恢复(recover)是一个内置函数, 用于重新控制恐慌(panic)协程(goroutine)。 使得程序能正常执行下去。
恢复功能的语法申请如下所示,
func recover() interface{}
只有在调用延迟(defer)函数时, 恢复(recover)才有用。在延迟(defer)函数内执行恢复(revocer)调用可以通过恢复正常执行来停止惊慌序列, 并检索传递给恐慌调用的错误值。如果在defer函数之外调用revocer, 则不会停止恐慌序列。
让我们修改程序, 使用revocer来恢复恐慌,使程序正常执行。
package mainimport ("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")}
第7行中的recoverName()函数调用recover() , 它返回传递给panic的调用的值。在这里, 我们只是打印在8行恢复返回的值。recoverName()在fullName函数的第14行中被延迟。
当fullNamepanic时, 将调用延迟的函数recoverName() , 它使用recover()来停止恐慌序列。
此程序将打印,
recovered from runtime error: last name cannot be nilreturned normally from maindeferred call in main
当程序在19行中死机时, 将调用延迟的recoverName函数, 然后调用recover()重新控制惊慌 goroutine。8行中的recover()调用返回来自恐慌的参数, 因此它打印,
recovered from runtime error: last name cannot be nil
执行recover()后, 恐慌停止, 并且控制返回给调用方, 在这种情况下, main函数和程序继续正常地从main中的29行执行, 在panic之后。它打印returned normally from main, 随后打印deferred call in main
只有在从同一个goroutine调用时才能恢复。在不同的 goroutine 中发生的恐慌是不可能恢复的。让我们用一个例子来理解这一点。
package mainimport ("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")}
在上面的程序中, 函数b()在第23行发生panic。函数a()调用一个延迟函数recovery() , 它用于从panic中恢复。第17行的函数b()是在一个单独 协程(goroutine)里。下一行中的Sleep只是为了确保程序在函数b() 完成运行之前不会终止。
你认为这个程序的输出是什么?恐慌会恢复吗?答案是不。恐慌将无法恢复。这是因为恢复函数存在于不同的协程(gouroutine)中, 并且在不同 goroutine 中的函数b()中发生了死机。因此, 恢复是不可能的。
运行此程序将输出,
Inside AInside Bpanic: oh! B panickedgoroutine 5 [running]:main.b()/tmp/sandbox388039916/main.go:23 +0x80created by main.a/tmp/sandbox388039916/main.go:17 +0xc0
您可以从输出中看到未发生恢复。
如果在同一 goroutine 中调用了函数b() , 则会恢复机。
如果程序的17行被更改为
go b()
to
b()
由于在同一 goroutine 发生恐慌, 恢复将会发生。如果程序使用上面的更改运行, 它将输出,
Inside AInside Brecovered: oh! B panickednormally returned from main
恐慌也可能由运行时错误引起,例如数组越界访问。这相当于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接口。
让我们编写一个虚构的例子来创建运行时恐慌。
package mainimport ("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")}
在上面的程序中, 在第9行, 我们试图访问n[3] , 它是切片中无效的索引。这个程序会触发panic,输出如下,
panic: runtime error: index out of rangegoroutine 1 [running]:main.a()/tmp/sandbox780439659/main.go:9 +0x40main.main()/tmp/sandbox780439659/main.go:13 +0x20
您可能想知道是否有可能从运行时恐慌中恢复。答案是肯定的。让我们改变上面的程序,从恐慌中恢复过来。
package mainimport ("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")}
运行上述程序将输出,
Recovered runtime error: index out of rangenormally returned from main
从输出中你可以明白我们已经从恐慌中恢复过来。
如果我们恢复了恐慌,我们就释放了恐慌的堆栈跟踪。即使在恢复之后的上述程序中,我们也失去了堆栈跟踪。
有一种方法可以使用Debug包的PrintStack函数来打印堆栈跟踪
package mainimport ("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()在11行中打印堆栈跟踪信息。
这个程序将输出,
Recovered runtime error: index out of rangegoroutine 1 [running]:runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)/usr/local/go/src/runtime/debug/stack.go:24 +0xc0runtime/debug.PrintStack()/usr/local/go/src/runtime/debug/stack.go:16 +0x20main.r()/tmp/sandbox949178097/main.go:11 +0xe0panic(0xf0a80, 0x17cd50)/usr/local/go/src/runtime/panic.go:491 +0x2c0main.a()/tmp/sandbox949178097/main.go:18 +0x80main.main()/tmp/sandbox949178097/main.go:23 +0x20normally returned from main
从输出中, 您可以了解到, 首先会恢复panic, 并打印Recovered runtime error: index out of range。之后, 将打印堆栈跟踪。然后, 在panic恢复后打印normally returned from main。
这使我们结束了本教程。
下面是我们在本教程中学到的内容的简要回顾,
祝你今天开心。本教程也到此结束了。