什么是 panic?

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

但是在某些情况下程序不能简单地在异常情况下继续执行。在这种情况下,我们使用panic 来终止程序。当函数遇到 panic 时,将停止执行,执行所有 deferred 函数,然后程序返回其调用函数。此过程一直持续到当前 goroutine 的所有函数都返回,此时程序打印出 panic 消息,然后是堆栈跟踪,然后终止。当我们编写一个示例程序时,这个概念会更加清晰。

使用 recover 可以重新控制 panic 程序,本教程稍后将对此进行讨论。

panic recover 可以被认为类似于其他语言中的 try-catch-finally,但它很少使用,但使用时更优雅,代码更干净。

什么时候该使用 panic?

必须强调的是我们应该避免 panic 和** recover 。尽可能地使用 errors。只有在程序无法继续执行的情况下,才应该使用 panic recover **机制。

panic 有两个有效的用例

  1. 一个不可恢复的错误,程序不能简单地继续执行它。


    比如 Web 服务器无法绑定到所需端口。在这种情况下,panic 才是合理的,因为如果端口绑定本身失败没有别的办法。

  2. 来自程序__员的错误

    假设我们有一个方法它接受一个指针作为参数。有人用 nil 作为参数调用这个方法。在这种情况下,我们可能会 panic,因为调用一个带有 nil 参数的方法是程序员的错误,因为该方法期望得到一个有效指针。

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

以上是打印一个人全名的简单程序。第 7 行 fullName 函数输出人的全名。 此函数在第 8 行和 11 行分别检查 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 是如何工作的,以及当程序 panic 时如何打印堆栈跟踪。

第 19 行我们将 Elon 分配给 firstName。 第 20 行我们调用 fullName 函数,lastNamenil 。 因此,11 行的条件将得到满足,程序将会出现 panic。 遇到panic 时,程序执行终止,打印传递给 panic 的参数,然后打印堆栈跟踪。 因此,panic 之后不会执行第 14 15 行的代码。 该程序首先打印传递给 panic 函数的消息,

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

然后打印堆栈跟踪。

程序在 12 行 fullName 函数 panic,因此

  1. goroutine 1 [running]:
  2. main.fullName(0xc00006af58, 0x0)
  3. /tmp/sandbox210590465/prog.go:12 +0x193

将首先打印。然后堆栈中的下一项将被打印出来。在本例中,堆栈跟踪中的下一项是第 20 行 fullName 调用,它导致了这一行中发生的 panic ,因此打印下一个。

  1. main.main()
  2. /tmp/sandbox210590465/prog.go:20 +0x4d

现在我们已经达到了造成 panic 的顶层函数,上面没有更多的层了,所以没有什么可以打印的了。

再举一个例子


panic 也可能是由运行时发生的错误引起的,比如试图访问一个不存在于切片中的索引。

让我们来写一个人为的例子,它将产生一个由于超出边界的切片访问而引起的 panic。

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

Run in playground

在上面的程序的第 9 行中,我们试图访问 n[4],这是一个无效的切片索引。这个程序会输出,

  1. panic: runtime error: index out of range [4] with length 3
  2. goroutine 1 [running]:
  3. main.slicePanic()
  4. /tmp/sandbox942516049/prog.go:9 +0x1d
  5. main.main()
  6. /tmp/sandbox942516049/prog.go:13 +0x22

当 panic 时 defer

让我们回忆一下 panic 的作用。当一个函数遇到 panic 时,程序将被停止,任何 defer 的函数会被执行,然后程序返回给调用函数。此过程将一直存在,直到当前 goroutine 的所有函数都返回,此时程序将打印 panic 消息,然后跟踪堆栈,然后终止。

在上面的例子中,我们没有 defer 任何函数调用。如果存在 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

对上述程序所做的唯一更改是在第 8 行和第 20 行添加了 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

当程序第 13 行 panic 时,首先执行 defer 的函数调用,然后代码返回给执行 defer 函数的调用函数,以此类推,直到到达顶层调用函数。

在我们的例子中,fullName 函数第 8 行的 defer 语句首先执行。输出

  1. deferred call in fullName

然后代码返回到 main 函数,主函数执行 defer 调用,因此输出

  1. deferred call in main

现在代码已经到顶层函数,因此程序打印 panic 消息,然后跟踪堆栈,然后终止。

从 Panic 中 Recover

recover 是一个内置函数,用于重新控制 panic goroutine。

recover 函数签名如下

  1. func recover() interface{}

只有在调用 defer 函数时,recover 才有用。在一个 defer 函数中调用一个 recovery 可以恢复正常运行并停止 panic 序列,并检索传递给 panic 调用的错误值。如果在 defer 函数之外调用 recovery,它将不会停止 panic 序列。

让我们修改我们的程序,并使用 recovery 来恢复 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(),该函数返回传递给 panic 调用的值。这里我们在第 8 行只是打印 recover 返回的值。14 行 recoverName()fullName 函数中被 defer 。

fullName 出现 panic 时,将调用 deferr 函数 recoverName(),该函数使用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() 来重新控制 panic goroutine。 对第 8 行 recover() 的调用会从 panic 中返回参数,因此输出,

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

在执行 recover() 之后,panic 停止并且程序返回到调用者,在这种情况下,主函数和程序在 panic 之后继续从 main 右边的第 29 行正常执行😃。 它打印 returned normally from main 其次是 deferred call in main

让我们再看一个例子,在这个例子中,我们从访问一个切片的无效索引引起的 panic 中 recover 过来。

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

Run in playground

运行上面的程序将输出,

  1. Recovered runtime error: index out of range [4] with length 3
  2. normally returned from main

从输出可以了解到,我们已经从 panic 中 recover 过来了。

Recover 后获取堆栈跟踪


如果我们 recover 了 panic,我们就会丢失关于 panic 的堆栈跟踪。即使在上述程序recover 后,我们也丢失了堆栈跟踪。

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

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

Run in playground

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

该程序将输出,

  1. recovered from runtime error: last name cannot be nil
  2. goroutine 1 [running]:
  3. runtime/debug.Stack(0x37, 0x0, 0x0)
  4. /usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x9d
  5. runtime/debug.PrintStack()
  6. /usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x22
  7. main.recoverFullName()
  8. /tmp/sandbox771195810/prog.go:11 +0xb4
  9. panic(0x4a1b60, 0x4dc300)
  10. /usr/local/go-faketime/src/runtime/panic.go:969 +0x166
  11. main.fullName(0xc0000a2f28, 0x0)
  12. /tmp/sandbox771195810/prog.go:21 +0x1cb
  13. main.main()
  14. /tmp/sandbox771195810/prog.go:30 +0xc6
  15. returned normally from main
  16. deferred call in main

从输出中可以了解到,panic 被 recovered 并且打印 recovered from runtime error: last name cannot be nil。继而打印出堆栈跟踪。然后

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

是在 panic 被 recovered 后打印的。

Panic, Recover 和 Goroutines

Recover 仅在从同一 goroutine 调用时才起作用。从不同的 goroutine 发生的 panic 中 Recover 是不可能的。让我们用一个例子来理解这一点。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func recovery() {
  6. if r := recover(); r != nil {
  7. fmt.Println("recovered:", r)
  8. }
  9. }
  10. func sum(a int, b int) {
  11. defer recovery()
  12. fmt.Printf("%d + %d = %d\n", a, b, a+b)
  13. done := make(chan bool)
  14. go divide(a, b, done)
  15. <-done
  16. }
  17. func divide(a int, b int, done chan bool) {
  18. fmt.Printf("%d / %d = %d", a, b, a/b)
  19. done <- true
  20. }
  21. func main() {
  22. sum(5, 0)
  23. fmt.Println("normally returned from main")
  24. }

Run in playground

在上面的程序中,函数 divide() 在第 22 行会出现 恐慌,因为 b 为零,不可能将一个数字除以零。sum() 函数调用了一个延迟函数 recovery(),用来从 panic 中 recover。函数 divide() 在第 17 行被单独的 goroutine 调用。我们在第 18 行的 done channel 上等待,以确保 divide() 函数执行完成。

你认为程序的输出会是什么。panic 会不会被 recovered?答案是不会。panic 不会被 recovered。这是因为 recovery 函数存在于不同的 goroutine 中,而 panic 发生在不同 goroutine 的 divide() 函数中。因此,recovery 是不可能的。

运行此程序将输出,

  1. 5 + 0 = 5
  2. panic: runtime error: integer divide by zero
  3. goroutine 18 [running]:
  4. main.divide(0x5, 0x0, 0xc0000a2000)
  5. /tmp/sandbox877118715/prog.go:22 +0x167
  6. created by main.sum
  7. /tmp/sandbox877118715/prog.go:17 +0x1a9

你可以从输出中看到 recover 没有发生。

如果在同一个 goroutine 中调用函数 divide(),那么 panic 就会被 recover。

如果程序第 17 行

  1. go divide(a, b, done)

变成

  1. divide(a, b, done)

由于 panic 发生在同一个 goroutine,现在将 recover。如果程序运行上面的更改,它将输出,

  1. 5 + 0 = 5
  2. recovered: runtime error: integer divide by zero
  3. normally returned from main

本教程到此结束。

让我们快速回顾在本教程中学到的内容,

  • 什么是 panic?


  • 什么时候适合用 panic?


  • Panic 例子


  • 当 panic 时 defer


  • 从 Panic 中 Recover


  • Recover 后获取堆栈跟踪


  • Panic, Recover 和 Goroutines


原文链接

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