[关闭]
@phper 2018-03-13T18:08:32.000000Z 字数 7576 阅读 3587

32.恐慌和恢复 (Panic and Recover)

Golang

原文:https://golangbot.com/panic-and-recover/


欢迎访问Golang 系列教程中的第32章。也是本系列的最后一章。

什么是恐慌?

在 go 程序中处理异常情况的惯用方法是使用错误(errors)。对于程序中出现的大多数异常情况, 使用错误都是足够的。

但在某些情况下,程序不能在异常情况之后继续执行。在这种情况下, 我们可以使用panic来终止程序。当一个函数(function)遇到恐慌(panic)时,它的执行被停止,任何延迟(defer)的函数都会被执行,然后控制权返回给调用者。这个过程继续下去,直到当前goroutine的所有函数都返回到程序打印出恐慌消息的地方,然后是堆栈跟踪,然后终止。当我们编写一个示例程序时,这个概念会更加清晰。

可以使用recover重新获得对恐慌程序的控制, 我们将在本教程后面讨论这些问题。

恐慌和恢复和其他编程语言中的try-catch-finally语法相似,除了它很少使用之前,当然它在被使用时会更优雅,并且代码整洁干净。

何时应该使用恐慌(panic)?

一个重要的因素是, 您应该避免惊慌和恢复, 并尽可能使用错误。只有在程序无法继续执行的情况下, 才应使用恐慌和恢复机制。

有两种有效的恐慌使用案例。

  1. 一个无法恢复的错误, 程序不能简单地继续执行它。
    一个示例是 web 服务器无法绑定到所需的端口。在这种情况下, 如果端口绑定本身失败, 那么就有理由恐慌。

  2. 程序员错误。
    假设,我们有一个接受指针作为参数的方法,并有人使用nil参数来调用此方法。在这种情况下,我们可能会因程序员的错误而惊慌失措,以nil期望有效指针的参数调用方法。

恐慌示例

内置panic函数的语法如下,

func panic(interface{})  

当程序终止时, 传递给恐慌的参数将被打印出来。当我们编写一个示例程序时,这个用法就很清楚了。所以, 让我们马上做。

我们将从一个人为例子开始,来说明恐慌如何起作用。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func fullName(firstName *string, lastName *string) {
  6. if firstName == nil {
  7. panic("runtime error: first name cannot be nil")
  8. }
  9. if lastName == nil {
  10. panic("runtime error: last name cannot be nil")
  11. }
  12. fmt.Printf("%s %s\n", *firstName, *lastName)
  13. fmt.Println("returned normally from fullName")
  14. }
  15. func main() {
  16. firstName := "Elon"
  17. fullName(&firstName, nil)
  18. fmt.Println("returned normally from main")
  19. }

在playground上运行

上面是一个简单的程序,用来打印一个人的全名。第7行中的函数fullName用来打印一个人的全名。在第8和11行这个函数会检查 "名字" 和 "姓氏" 指针是否分别为nil。如果它是nil, 则该函数将使用相应的错误消息来调用panic。此错误消息将在程序终止时打印。

运行此程序将打印以下输出,

  1. panic: runtime error: last name cannot be nil
  2. goroutine 1 [running]:
  3. main.fullName(0x1040c128, 0x0)
  4. /tmp/sandbox135038844/main.go:12 +0x120
  5. main.main()
  6. /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, 因此,

  1. main.fullName(0x1040c128, 0x0)
  2. /tmp/sandbox135038844/main.go:12 +0x120

将首先打印。然后将打印堆栈中的下一项。在我们的案例中, 20 行是堆栈跟踪中的下一个项目, 它是导致此行发生恐慌的fullName调用, 因此

  1. main.main()
  2. /tmp/sandbox135038844/main.go:20 +0x80

会接着打印。现在我们已经达到了导致恐慌的顶层功能, 并且上面没有更多的级别, 因此没有更多的打印。

延迟恐慌

让我们回想一下恐慌是怎么回事。当某个函数遇到恐慌(panic)时, 它的执行被停止, 任何推迟(defer)的函数都会被执行, 然后控制权返回给调用者。此过程将继续, 直到当前 goroutine 的所有函数都返回时, 程序打印出紧急消息,随后是堆栈跟踪,然后终止。

在上面的例子中, 我们没有推迟任何函数调用。如果存在延迟(defer)函数调用, 则执行该函数,然后控件返回给调用者。

让我们稍微修改一下上面的示例, 然后使用延迟语句。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func fullName(firstName *string, lastName *string) {
  6. defer fmt.Println("deferred call in fullName")
  7. if firstName == nil {
  8. panic("runtime error: first name cannot be nil")
  9. }
  10. if lastName == nil {
  11. panic("runtime error: last name cannot be nil")
  12. }
  13. fmt.Printf("%s %s\n", *firstName, *lastName)
  14. fmt.Println("returned normally from fullName")
  15. }
  16. func main() {
  17. defer fmt.Println("deferred call in main")
  18. firstName := "Elon"
  19. fullName(&firstName, nil)
  20. fmt.Println("returned normally from main")
  21. }

在playground上运行

对上述程序所做的唯一更改是在第8和20行中添加延迟函数调用。

此程序打印,

  1. deferred call in fullName
  2. deferred call in main
  3. panic: runtime error: last name cannot be nil
  4. goroutine 1 [running]:
  5. main.fullName(0x1042bf90, 0x0)
  6. /tmp/sandbox060731990/main.go:13 +0x280
  7. main.main()
  8. /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来恢复恐慌,使程序正常执行。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func recoverName() {
  6. if r := recover(); r!= nil {
  7. fmt.Println("recovered from ", r)
  8. }
  9. }
  10. func fullName(firstName *string, lastName *string) {
  11. defer recoverName()
  12. if firstName == nil {
  13. panic("runtime error: first name cannot be nil")
  14. }
  15. if lastName == nil {
  16. panic("runtime error: last name cannot be nil")
  17. }
  18. fmt.Printf("%s %s\n", *firstName, *lastName)
  19. fmt.Println("returned normally from fullName")
  20. }
  21. func main() {
  22. defer fmt.Println("deferred call in main")
  23. firstName := "Elon"
  24. fullName(&firstName, nil)
  25. fmt.Println("returned normally from main")
  26. }

在playground上运行

第7行中的recoverName()函数调用recover() , 它返回传递给panic的调用的值。在这里, 我们只是打印在8行恢复返回的值。recoverName()fullName函数的第14行中被延迟。

fullNamepanic时, 将调用延迟的函数recoverName() , 它使用recover()来停止恐慌序列。

此程序将打印,

  1. recovered from runtime error: last name cannot be nil
  2. returned normally from main
  3. 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

恐慌、恢复和 Goroutines

只有在从同一个goroutine调用时才能恢复。在不同的 goroutine 中发生的恐慌是不可能恢复的。让我们用一个例子来理解这一点。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func recovery() {
  7. if r := recover(); r != nil {
  8. fmt.Println("recovered:", r)
  9. }
  10. }
  11. func a() {
  12. defer recovery()
  13. fmt.Println("Inside A")
  14. go b()
  15. time.Sleep(1 * time.Second)
  16. }
  17. func b() {
  18. fmt.Println("Inside B")
  19. panic("oh! B panicked")
  20. }
  21. func main() {
  22. a()
  23. fmt.Println("normally returned from main")
  24. }

在playground上运行

在上面的程序中, 函数b()在第23行发生panic。函数a()调用一个延迟函数recovery() , 它用于从panic中恢复。第17行的函数b()是在一个单独 协程(goroutine)里。下一行中的Sleep只是为了确保程序在函数b() 完成运行之前不会终止。

你认为这个程序的输出是什么?恐慌会恢复吗?答案是不。恐慌将无法恢复。这是因为恢复函数存在于不同的协程(gouroutine)中, 并且在不同 goroutine 中的函数b()中发生了死机。因此, 恢复是不可能的。

运行此程序将输出,

  1. Inside A
  2. Inside B
  3. panic: oh! B panicked
  4. goroutine 5 [running]:
  5. main.b()
  6. /tmp/sandbox388039916/main.go:23 +0x80
  7. created by main.a
  8. /tmp/sandbox388039916/main.go:17 +0xc0

您可以从输出中看到未发生恢复。

如果在同一 goroutine 中调用了函数b() , 则会恢复机。

如果程序的17行被更改为

go b()

to

b()  

由于在同一 goroutine 发生恐慌, 恢复将会发生。如果程序使用上面的更改运行, 它将输出,

  1. Inside A
  2. Inside B
  3. recovered: oh! B panicked
  4. normally returned from main

运行时恐慌

恐慌也可能由运行时错误引起,例如数组越界访问。这相当于panic使用由接口类型runtime.Error定义的参数调用内置函数。runtime.Error的定义在如下,

  1. type Error interface {
  2. error
  3. // RuntimeError is a no-op function but
  4. // serves to distinguish types that are run time
  5. // errors from ordinary errors: a type is a
  6. // run time error if it has a RuntimeError method.
  7. RuntimeError()
  8. }

runtime.Error接口实现了内置的error接口。

让我们编写一个虚构的例子来创建运行时恐慌。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func a() {
  6. n := []int{5, 7, 4}
  7. fmt.Println(n[3])
  8. fmt.Println("normally returned from a")
  9. }
  10. func main() {
  11. a()
  12. fmt.Println("normally returned from main")
  13. }

在playground上运行

在上面的程序中, 在第9行, 我们试图访问n[3] , 它是切片中无效的索引。这个程序会触发panic,输出如下,

  1. panic: runtime error: index out of range
  2. goroutine 1 [running]:
  3. main.a()
  4. /tmp/sandbox780439659/main.go:9 +0x40
  5. main.main()
  6. /tmp/sandbox780439659/main.go:13 +0x20

您可能想知道是否有可能从运行时恐慌中恢复。答案是肯定的。让我们改变上面的程序,从恐慌中恢复过来。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func r() {
  6. if r := recover(); r != nil {
  7. fmt.Println("Recovered", r)
  8. }
  9. }
  10. func a() {
  11. defer r()
  12. n := []int{5, 7, 4}
  13. fmt.Println(n[3])
  14. fmt.Println("normally returned from a")
  15. }
  16. func main() {
  17. a()
  18. fmt.Println("normally returned from main")
  19. }

在playground上运行

运行上述程序将输出,

  1. Recovered runtime error: index out of range
  2. normally returned from main

从输出中你可以明白我们已经从恐慌中恢复过来。

恢复后获取堆栈跟踪

如果我们恢复了恐慌,我们就释放了恐慌的堆栈跟踪。即使在恢复之后的上述程序中,我们也失去了堆栈跟踪。

有一种方法可以使用Debug包的PrintStack函数来打印堆栈跟踪

  1. package main
  2. import (
  3. "fmt"
  4. "runtime/debug"
  5. )
  6. func r() {
  7. if r := recover(); r != nil {
  8. fmt.Println("Recovered", r)
  9. debug.PrintStack()
  10. }
  11. }
  12. func a() {
  13. defer r()
  14. n := []int{5, 7, 4}
  15. fmt.Println(n[3])
  16. fmt.Println("normally returned from a")
  17. }
  18. func main() {
  19. a()
  20. fmt.Println("normally returned from main")
  21. }

在playground上奔跑

在上面的程序中, 我们使用debug.PrintStack()在11行中打印堆栈跟踪信息。

这个程序将输出,

  1. Recovered runtime error: index out of range
  2. goroutine 1 [running]:
  3. runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)
  4. /usr/local/go/src/runtime/debug/stack.go:24 +0xc0
  5. runtime/debug.PrintStack()
  6. /usr/local/go/src/runtime/debug/stack.go:16 +0x20
  7. main.r()
  8. /tmp/sandbox949178097/main.go:11 +0xe0
  9. panic(0xf0a80, 0x17cd50)
  10. /usr/local/go/src/runtime/panic.go:491 +0x2c0
  11. main.a()
  12. /tmp/sandbox949178097/main.go:18 +0x80
  13. main.main()
  14. /tmp/sandbox949178097/main.go:23 +0x20
  15. normally returned from main

从输出中, 您可以了解到, 首先会恢复panic, 并打印Recovered runtime error: index out of range。之后, 将打印堆栈跟踪。然后, 在panic恢复后打印normally returned from main

这使我们结束了本教程。

下面是我们在本教程中学到的内容的简要回顾,

祝你今天开心。本教程也到此结束了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注