32. panic 和 recover

panic 和 recover

欢迎来到 Golang 系列教程的第 32 篇。

什么是 panic?

在 Go 语言中,程序中一般是使用错误来处理异常情况。对于程序中出现的大部分异常情况,错误就已经够用了。

但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。在编写一个示例程序后,我们就能很好地理解这个概念了。

在本教程里,我们还会接着讨论,当程序发生 panic 时,使用 recover 可以重新获得对该程序的控制。

可以认为 panicrecover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panicrecover。而当我们使用了 panicrecover 时,也会比 try-catch-finally 更加优雅,代码更加整洁。

什么时候应该使用 panic?

需要注意的是,你应该尽可能地使用错误,而不是使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制

panic 有两个合理的用例。

  1. 发生了一个不能恢复的错误,此时程序不能继续运行
    一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。

  2. 发生了一个编程上的错误
    假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

panic 示例

内建函数 panic 的签名如下所示:

func panic(interface{})

当程序终止时,会打印传入 panic 的参数。我们写一个示例,你就会清楚它的用途了。我们现在就开始吧。

我们会写一个例子,来展示 panic 如何工作。

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 行分别检查了 firstNamelastName 的指针是否为 nil。如果是 nilfullName 函数会调用含有不同的错误信息的 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 是如何工作的,并且思考当程序发生 panic 时,会怎样打印堆栈跟踪。

在第 19 行,我们将 Elon 赋值给了 firstName。在第 20 行,我们调用了 fullName 函数,其中 lastName 等于 nil。因此,满足了第 11 行的条件,程序发生 panic。当出现了 panic 时,程序就会终止运行,打印出传入 panic 的参数,接着打印出堆栈跟踪。因此,第 14 行和第 15 行的代码并不会在发生 panic 之后执行。程序首先会打印出传入 panic 函数的信息:

panic: runtime error: last name cannot be empty

接着打印出堆栈跟踪。

程序在 fullName 函数的第 12 行发生 panic,因此,首先会打印出如下所示的输出。

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

接着会打印出堆栈的下一项。在本例中,堆栈跟踪中的下一项是第 20 行(因为发生 panic 的 fullName 调用就在这一行),因此接下来会打印出:

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

现在我们已经到达了导致 panic 的顶层函数,这里没有更多的层级,因此结束打印。

发生 panic 时的 defer

我们重新总结一下 panic 做了什么。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止

在上面的例子中,我们没有延迟调用任何函数。如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。

我们来修改上面的示例,使用一个延迟语句。

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 行添加了延迟函数的调用。

该函数会打印:

This program prints,

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 行发生 panic 时,首先执行了延迟函数,接着控制返回到函数调用方,调用方的延迟函数继续运行,直到到达顶层调用函数。

在我们的例子中,首先执行 fullName 函数中的 defer 语句(第 8 行)。程序打印出:

deferred call in fullName

接着程序返回到 main 函数,执行了 main 函数的延迟调用,因此会输出:

deferred call in main

现在程序控制到达了顶层函数,因此该函数会打印出 panic 信息,然后是堆栈跟踪,最后终止程序。

recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。

recover 函数的标签如下所示:

func recover() interface{}

只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。如果在延迟函数的外部调用 recover,就不能停止 panic 续发事件。

我们来修改一下程序,在发生 panic 之后,使用 recover 来恢复正常的运行。

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 的传参。在这里,我们只是打印出 recover 的返回值(第 8 行)。在 fullName 函数内,我们在第 14 行延迟调用了 recoverNames()

fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来停止 panic 续发事件。

该程序会输出:

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

当程序在第 19 行发生 panic 时,会调用延迟函数 recoverName,它反过来会调用 recover() 来重新获得 panic 协程的控制。第 8 行调用了 recover,返回了 panic 的传参,因此会打印:

recovered from  runtime error: last name cannot be nil

在执行完 recover() 之后,panic 会停止,程序控制返回到调用方(在这里就是 main 函数),程序在发生 panic 之后,从第 29 行开始会继续正常地运行。程序会打印 returned normally from main,之后是 deferred call in main

panic,recover 和 Go 协程

只有在相同的 Go 协程中调用 recover 才管用。recover 不能恢复一个不同协程的 panic。我们用一个例子来理解这一点。

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() 作为一个不同的协程来调用。下一行的 Sleep 只是保证 a()b() 运行结束之后才退出。

你认为程序会输出什么?panic 能够恢复吗?答案是否定的,panic 并不会恢复。因为调用 recovery 的协程和 b() 中发生 panic 的协程并不相同,因此不可能恢复 panic。

运行该程序会输出:

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

从输出可以看出,panic 没有恢复。

如果函数 b() 在相同的协程里调用,panic 就可以恢复。

如果程序的第 17 行由 go b() 修改为 b(),就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。如果运行这个修改后的程序,会输出:

Inside A  
Inside B  
recovered: oh! B panicked  
normally returned from main

运行时 panic

运行时错误(如数组越界)也会导致 panic。这等价于调用了内置函数 panic,其参数由接口类型 runtime.Error 给出。runtime.Error 接口的定义如下:

type Error interface {  
    error
    
    
    
    
    RuntimeError()
}

runtime.Error 接口满足内建接口类型 error

我们来编写一个示例,创建一个运行时 panic。

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

你也许想知道,是否可以恢复一个运行时 panic?当然可以!我们来修改一下上面的代码,恢复这个 panic。

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

从输出可以知道,我们已经恢复了这个 panic。

恢复后获得堆栈跟踪

当我们恢复 panic 时,我们就释放了它的堆栈跟踪。实际上,在上述程序里,恢复 panic 之后,我们就失去了堆栈跟踪。

有办法可以打印出堆栈跟踪,就是使用 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 上运行

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

该程序会输出:

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

本教程到此结束。

简单概括一下本教程讨论的内容:

  • 什么是 panic?
  • 什么时候应该使用 panic?
  • panic 示例
  • 发生 panic 时的 defer
  • recover
  • panic,recover 和 Go 协程
  • 运行时 panic
  • 恢复后获得堆栈跟踪

祝你愉快。

上一教程 – 自定义错误

下一教程 – 头等函数

若文章对你有帮助,可以点赞或打赏支持我们。发布者:Aurora,转载请注明出处:http://61.174.243.28:13541/AY-knowledg-hub/32-panic-%e5%92%8c-recover/

(0)
AuroraAurora站点维系者
上一篇 2023年 12月 5日 下午5:39
下一篇 2023年 12月 5日 下午5:41

相关推荐

  • tload

    文章目录tload补充说明语法选项参数实例 tload 显示系统负载状况 补充说明 tload命令 以图形化的方式输出当前系统的平均负载到指定的终端。假设不给予终端机编号,则会在执…

    入门教程 2024年 3月 11日
  • HTML 图像 

    文章目录HTML 图像- 图像标签( <img>)和源属性(Src)HTML 图像- Alt属性HTML 图像- 设置图像的高度与宽度基本的注意事项 – 有…

    2023年 4月 13日
  • HTTP 状态码

    当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)…

    2023年 5月 14日
  • inotifywait

    文章目录inotifywait补充说明安装inotify-toolsinotify相关参数inotifywait命令使用inotifywait命令参数可监听的事件 inotifyw…

    入门教程 2023年 12月 19日
  • bind

    文章目录bind补充说明语法选项实例 bind 显示或设置键盘按键与其相关的功能 补充说明 bind命令 用于显示和设置命令行的键盘序列绑定功能。通过这一命令,可以提高命令行中操作…

    入门教程 2023年 12月 6日
  • openssl

    文章目录openssl补充说明实例 openssl 强大的安全套接字层密码库 补充说明 OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理…

    入门教程 2024年 3月 1日
  • arpwatch

    文章目录arpwatch补充说明语法选项 arpwatch 监听网络上ARP的记录 补充说明 arpwatch命令 用来监听网络上arp的记录。 语法 arpwatch(选项) 选…

    入门教程 2023年 12月 6日
  • fmt

    文章目录fmt补充说明语法选项参数 fmt 读取文件后优化处理并输出 补充说明 fmt命令 读取文件的内容,根据选项的设置对文件格式进行简单的优化处理,并将结果送到标准输出设备。 …

    入门教程 2023年 12月 14日
  • resize

    文章目录resize补充说明语法选项实例 resize 命令设置终端机视窗的大小 补充说明 resize命令 命令设置终端机视窗的大小。执行resize指令可设置虚拟终端机的视窗大…

    入门教程 2024年 3月 4日
  • xlsclients

    文章目录xlsclients补充说明语法选项 xlsclients 列出显示器中的客户端应用程序 补充说明 xlsclients命令 用来列出显示器中的客户端应用程序。 语法 xl…

    入门教程 2024年 3月 11日
Translate »