@phper
2018-03-13T18:08:32.000000Z
字数 7576
阅读 3485
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 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")
}
上面是一个简单的程序,用来打印一个人的全名。第7行中的函数fullName
用来打印一个人的全名。在第8和11行这个函数会检查 "名字" 和 "姓氏" 指针是否分别为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。在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 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(0x1042bf90, 0x0)
/tmp/sandbox060731990/main.go:13 +0x280
main.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 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")
}
第7行中的recoverName()
函数调用recover()
, 它返回传递给panic
的调用的值。在这里, 我们只是打印在8行恢复返回的值。recoverName()
在fullName
函数的第14行中被延迟。
当fullName
panic时, 将调用延迟的函数recoverName()
, 它使用recover()
来停止恐慌序列。
此程序将打印,
recovered from runtime error: last name cannot be nil
returned normally from main
deferred 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 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")
}
在上面的程序中, 函数b()
在第23行发生panic
。函数a()
调用一个延迟函数recovery()
, 它用于从panic中恢复。第17行的函数b()
是在一个单独 协程(goroutine)里。下一行中的Sleep
只是为了确保程序在函数b()
完成运行之前不会终止。
你认为这个程序的输出是什么?恐慌会恢复吗?答案是不。恐慌将无法恢复。这是因为恢复函数存在于不同的协程(gouroutine)中, 并且在不同 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() , 则会恢复机。
如果程序的17行被更改为
go b()
to
b()
由于在同一 goroutine 发生恐慌, 恢复将会发生。如果程序使用上面的更改运行, 它将输出,
Inside A
Inside B
recovered: oh! B panicked
normally 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 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")
}
在上面的程序中, 在第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
您可能想知道是否有可能从运行时恐慌中恢复。答案是肯定的。让我们改变上面的程序,从恐慌中恢复过来。
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")
}
运行上述程序将输出,
Recovered runtime error: index out of range
normally returned from main
从输出中你可以明白我们已经从恐慌中恢复过来。
如果我们恢复了恐慌,我们就释放了恐慌的堆栈跟踪。即使在恢复之后的上述程序中,我们也失去了堆栈跟踪。
有一种方法可以使用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()
在11行中打印堆栈跟踪信息。
这个程序将输出,
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
。
这使我们结束了本教程。
下面是我们在本教程中学到的内容的简要回顾,
祝你今天开心。本教程也到此结束了。