「GCTT 出品」Go 系列教程32. panic 和 recover

Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!


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

什么是 panic?

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

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

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

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

什么时候应该使用 panic?

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

panic 有两个合理的用例。

  1. 发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。
  2. 发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

panic 示例

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

func panic(interface{})

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

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

上面的程序很简单,会打印一个人的全名。第 7 行的 fullName 函数会打印出一个人的全名。该函数在第 8 行和第 11 行分别检查了 firstName 和 lastName 的指针是否为 nil。如果是 nil,fullName 函数会调用含有不同的错误信息的 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 信息,接着打印出堆栈跟踪,最后程序终止。

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

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

上述代码中,我们只修改了两处,分别在第 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 来恢复正常的运行。

在第 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。我们用一个例子来理解这一点。

在上面的程序中,函数 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 接口的定义如下:

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

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

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

运行上面程序会输出:

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

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

恢复后获得堆栈跟踪

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

有办法可以打印出堆栈跟踪,就是使用 Debug 包中的 PrintStack 函数。

在上面的程序中,我们在第 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
  • 恢复后获得堆栈跟踪

祝你愉快。

上一教程 - 「GCTT 出品」Go 系列教程31. 自定义错误

下一教程 - 头等函数


历史文章:

「GCTT 出品」Go 系列教程1. 介绍与安装

「GCTT 出品」Go 系列教程2. Hello World

「GCTT 出品」Go 系列教程3. 变量

「GCTT 出品」Go 系列教程4. 类型

「GCTT 出品」Go 系列教程5. 常量

「GCTT 出品」Go 系列教程6. 函数(Function)

「GCTT 出品」Go 系列教程7. 包

Go 系列教程8. if-else 语句

「GCTT 出品」Go 系列教程9. 循环

「GCTT 出品」Go 系列教程10. switch 语句

「GCTT 出品」Go 系列教程11. 数组和切片

「GCTT 出品」Go 系列教程12. 可变参数函数

「GCTT 出品」Go 系列教程13. Maps

「GCTT 出品」Go 系列教程14. 字符串

「GCTT 出品」Go 系列教程15. 指针

「GCTT 出品」Go 系列教程16. 结构体,这一篇就够

「GCTT 出品」Go 系列教程17. 超全的方法教程

「GCTT 出品」Go 系列教程18. 接口(一)

「GCTT 出品」Go 系列教程19. 接口(二)

「GCTT 出品」Go 系列教程20. 并发入门

「GCTT 出品」Go 系列教程21. Go 协程

「GCTT 出品」Go 系列教程22. 信道(channel)

「GCTT 出品」Go 系列教程23. 缓冲信道和工作池

「GCTT 出品」Go 系列教程24. Select

「GCTT 出品」Go 系列教程25. Mutex

「GCTT 出品」Go 系列教程26. 结构体取代类

「GCTT 出品」Go 系列教程27. 组合取代继承

「GCTT 出品」Go 系列教程28. 多态

「GCTT 出品」Go 系列教程29. Defer

「GCTT 出品」Go 系列教程30. 错误处理

「GCTT 出品」Go 系列教程31. 自定义错误

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

热门产品

php编程基础教程.pptx|php编程培训,php,编程,基础,教程,pptx
php编程基础教程.pptx

历史上的今天:04月20日

ThinkPHP5快速入门基础

ThinkPHP5快速入门基础一、基础快速入门 ( 一 ) :基础本章介绍了 ThinkPHP5 .0 的安装及基本使用 ,并给出了一个最简单的示例带你了解如何开始开发 ,主要包 含 :简介官网下载 omposer安装和更新CGit下载和更新目录结构运行环境入口文件调试模式控制器视图读取数据总结在学习 ThinkPHP5.0 之前 ,如果你还不理解面向对象和命名空间的概念 ,建议首先去PHP手册恶

ThinkPHP5快速入门

ThinkPHP5快速入门目 录零、序言一、基础二、URL和路由三、请求和响应四、数据库五、查询语言六、模型和关联 (1)模型定义 (2)基础操作 (3)读取器和修改器 (4)类型转换和自动完成 (5)查询范围 (6)输入和验证 (7)关联 (8)模型输出七、视图和模板八、调试和日志九、API开发十、命令行工具十一、扩展十二、杂项SessionCookie验证

热门专题

安徽中源管业|安徽中源管业,安徽中源管业mpp电力管,安徽中源管业cpvc电力管,安徽中源管业pe穿线管,安徽中源管业电力管,安徽中源管业排水管,安徽中源管业通信管,安徽中源管业管材
安徽中源管业
中源管业|中源管业,中源管业公司,中源管业有限公司,中源管业电话,中源管业地址,中源管业电力管,中源管业mpp电力管,中源管业cpvc电力管,中源管业pe穿线管
中源管业
弥勒综合高中|弥勒综合高中
弥勒综合高中
大理科技管理学校|大理科技管理学校,大理科技,大理科技中等职业技术学校,大理科技管理中等职业技术学校,大理科技学校
大理科技管理学校
易捷尔高职单招|易捷尔高职单招,易捷尔高职单招培训,单招分数线,单招录取分数线,高职单招学校分数线
易捷尔高职单招
金诺幼儿园(春城路金诺幼儿园)|昆明官渡区幼儿园,幼儿园报名,官渡区幼儿园,春城路幼儿园,幼儿园招生,学前班,昆明幼儿园,金诺幼儿园,环城南路幼儿园,石井路幼儿园
金诺幼儿园(春城路金诺幼儿园)
大理科技管理学校|大理科技管理中等职业技术学校,大理市科技管理中等职业技术学校
大理科技管理学校
开放大学|开放大学报名,开放大学报考,开放大学,什么是开放大学,开放大学学历,开放大学学费,开放大学报名条件,开放大学报名时间,开放大学学历,开放大学专业
开放大学

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部