[译] Part 32: golang 中的 panic 和 recover

https://juejin.im/post/5c9f9728f265da30dc7abf6b

什么是panic?

处理Go中异常情况的惯用方法是使用errors,对于程序中出现的大多数异常情况,errors就足够了。

但是在某些情况下程序不能在异常情况下继续正常执行。在这种情况下,我们使用panic来终止程序。函数遇到panic时将会停止执行,如果有defer的话就执行defer延迟函数,然后返回其调用者。此过程一直持续到当前goroutine的所有函数都返回,然后打印出panic信息,然后是堆栈信息,然后程序终止。待会儿用一个例子来解释,这个概念就会更加清晰一些了。

我们可以使用recover函数恢复被panic终止的程序,将在本教程后面讨论。

panic和recover有点类似于其他语言中的try-catch-finally语句,但是前者使用的比较少,而且使用时更优雅代码也更简洁。

什么时候应该用panic?

一般情况下我们应该避免使用panic和recover,尽可能使用errors。只有在程序无法继续执行的情况下才应该使用panic和recover。

两个panic典型应用场景
  1. 不可恢复的错误,让程序不能继续进行。 比如说Web服务器无法绑定到指定端口。在这种情况下,panic是合理的,因为如果端口绑定失败接下来的逻辑继续也是没有意义的。

  2. coder的人为错误 假设我们有一个接受指针作为参数的方法,然而使用了nil作为参数调用此方法。在这种情况下,我们可以用panic,因为该方法需要一个有效的指针。

panic示例

panic函数的定义

  1. func panic(interface{})

当程序终止时,参数会传递给panic函数打印出来。看看下面例子的panic是如何使用的。

  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. }

Run in playground

上面这段代码,fullName函数功能是打印一个人的全名。此函数检查firstName和lastName指针是否为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。然后调用fullName函数,其中lastName参数为nil。因此,第11行将触发panic。当触发panic时,程序执行就终止了,然后打印传递给panic的内容,最后打印堆栈跟踪信息。因此14行以后的代码不会被执行。 该程序首先打印传递给panic函数的内容,

  1. panic: runtime error: last name cannot be nil

然后打印堆栈跟踪信息。 该程序在12行触发panic,因此,

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

将被首先打印。然后将打印堆栈中的下一个内容,

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

现在已经返回到了造成panic的顶层main函数,因此打印结束。

defer函数

我们回想一下panic的作用。当函数遇到panic时,将会终止panic后面代码的执行,如果函数体包含有defer函数的话会执行完defer函数。然后返回其调用者。此过程一直持续到当前goroutine的所有函数都返回,此时程序打印出panic内容,然后是堆栈跟踪信息,然后终止。

在上面的示例中,我们没有任何defer函数的调用。修改下上面的例子,来看看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. }

Run in playground 对之前代码所做的唯一更改是在fullName函数和main函数中第一行添加了defer函数调用。 运行的输出,

  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

当发生panic时,首先执行defer函数,然后到下一个defer调用,依此类推,直到达到顶层调用者。

在我们的例子中,defer声明在fullName函数的第一行。首先执行fullName函数。打印

  1. deferred call in fullName

然后调用返回到main函数的defer,

  1. deferred call in main

现在调用已返回到顶层函数,然后程序打印panic内容,然后是堆栈跟踪信息,然后终止。

recover函数

recover是一个内置函数,用于goroutine从panic的中断状况中恢复。 函数定义如下,

  1. func recover() interface{}

recover只有在defer函数内部调用时才有效。defer函数内通过调用recover可以让panic中断的程序恢复正常执行,调用recover会返回panic的内容。如果在defer函数之外调用recover,它将不会停止panic序列。

修改一下,使用recover来让panic恢复正常执行。

  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. }

Run in playground 第7行调用了recoverName函数。这里打印了recover返回的值, 发现recover返回的是panic的内容。

打印如下,

  1. recovered from runtime error: last name cannot be nil
  2. returned normally from main
  3. deferred call in main

程序在19行触发panic,defer函数recoverName通过调用recover来重新控制该goroutine,

  1. recovered from runtime error: last name cannot be nil

在执行recover之后,panic停止并且返回到调用者,main函数和程序在触发panic之后将继续从第29行执行。然后打印,

  1. returned normally from main
  2. deferred call in main

Panic, Recover 和 Goroutines

recover仅在从同一个goroutine调用时才起作用。从不同的goroutine触发的panic中recover是不可能的。再来一个例子来加深理解。

  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. }

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函数。因此无法恢复。 运行的输出,

  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,那么panic就会被恢复。 在第17行把, go b() 换成 b() 那么会输出,

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

运行时的panic

panic还可能由运行时的错误引起,例如数组越界访问。这相当于使用由接口类型runtime.Error定义的参数调用内置函数panic。 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

让我们写一个人为的例子来创建运行时panic。

  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. }

Run in 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

您可能想知道是否运行中的panic能够被恢复。答案是肯定的。让我们修改上面的程序,让panic恢复过来。

  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. }

Run in playground 执行后输出,

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

显然可以看到panic被恢复了。

recover后获取堆栈信息

我们恢复了panic,但是丢失了这次panic的堆栈调用的信息。 有一种方法可以解决这个,就是使用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. }

Run in playground 在11行调用了debug.PrintStack,可以看到随后输出,

  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

ft_authoradmin  ft_create_time2019-08-03 16:41
 ft_update_time2019-08-03 16:41