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

原文: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{})  

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

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

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")
}

在playground上运行

上面是一个简单的程序,用来打印一个人的全名。第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")
}

在playground上运行

对上述程序所做的唯一更改是在第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")
}

在playground上运行

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

fullNamepanic时, 将调用延迟的函数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

恐慌、恢复和 Goroutines

只有在从同一个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")
}

在playground上运行

在上面的程序中, 函数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")
}

在playground上运行

在上面的程序中, 在第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")
}

在playground上运行

运行上述程序将输出,

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")
}

在playground上运行

在上面的程序中, 我们使用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

这使我们结束了本教程。

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

  • 什么是恐慌?
  • 何时应使用恐慌?
  • 恐慌示例
  • 惊慌时延迟
  • 恢复
  • 恐慌、恢复和 Goroutines
  • 运行时恐慌
  • 恢复后获取堆栈跟踪

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

你可能感兴趣的:(Golang,tutorial,series)