Go语言核心36讲(Go语言进阶技术十六)--学习笔记
22 | panic函数、recover函数以及defer语句(下)
我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因。
如果一个 panic 是我们在无意间引发的,那么其中的值只能由 Go 语言运行时系统给定。但是,当我们使用panic函数有意地引发一个 panic 的时候,却可以自行指定其包含的值。我们今天的第一个问题就是针对后一种情况提出的。
知识扩展
问题 1:怎样让 panic 包含一个值,以及应该让它包含什么样的值?
这其实很简单,在调用panic函数时,把某个值作为参数传给该函数就可以了。由于panic函数的唯一一个参数是空接口(也就是interface{})类型的,所以从语法上讲,它可以接受任何类型的值。
但是,我们最好传入error类型的错误值,或者其他的可以被有效序列化的值。这里的“有效序列化”指的是,可以更易读地去表示形式转换。
还记得吗?对于fmt包下的各种打印函数来说,error类型值的Error方法与其他类型值的String方法是等价的,它们的唯一结果都是string类型的。
我们在通过占位符%s打印这些值的时候,它们的字符串表示形式分别都是这两种方法产出的。
一旦程序异常了,我们就一定要把异常的相关信息记录下来,这通常都是记到程序日志里。
我们在为程序排查错误的时候,首先要做的就是查看和解读程序日志;而最常用也是最方便的日志记录方式,就是记下相关值的字符串表示形式。
所以,如果你觉得某个值有可能会被记到日志里,那么就应该为它关联String方法。如果这个值是error类型的,那么让它的Error方法返回你为它定制的字符串表示形式就可以了。
对于此,你可能会想到fmt.Sprintf,以及fmt.Fprintf这类可以格式化并输出参数的函数。
是的,它们本身就可以被用来输出值的某种表示形式。不过,它们在功能上,肯定远不如我们自己定义的Error方法或者String方法。因此,为不同的数据类型分别编写这两种方法总是首选。
可是,这与传给panic函数的参数值又有什么关系呢?其实道理是相同的。至少在程序崩溃的时候,panic 包含的那个值字符串表示形式会被打印出来。
另外,我们还可以施加某种保护措施,避免程序的崩溃。这个时候,panic 包含的值会被取出,而在取出之后,它一般都会被打印出来或者记录到日志里。
既然说到了应对 panic 的保护措施,我们再来看下面一个问题。
问题 2:怎样施加应对 panic 的保护措施,从而避免程序崩溃?
Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需任何参数,并且会返回一个空接口类型的值。
如果用法正确,这个值实际上就是即将恢复的 panic 包含的值。并且,如果这个 panic 是因我们调用panic函数而引发的,那么该值同时也会是我们此次调用panic函数时,传入的参数值副本。请注意,这里强调用法的正确。我们先来看看什么是不正确的用法。
package main
import (
"fmt"
"errors"
)
func main() {
fmt.Println("Enter function main.")
// 引发panic。
panic(errors.New("something wrong"))
p := recover()
fmt.Printf("panic: %s\n", p)
fmt.Println("Exit function main.")
}
在上面这个main函数中,我先通过调用panic函数引发了一个 panic,紧接着想通过调用recover函数恢复这个 panic。可结果呢?你一试便知,程序依然会崩溃,这个recover函数调用并不会起到任何作用,甚至都没有机会执行。
还记得吗?我提到过 panic 一旦发生,控制权就会讯速地沿着调用栈的反方向传播。所以,在panic函数调用之后的代码,根本就没有执行的机会。
那如果我把调用recover函数的代码提前呢?也就是说,先调用recover函数,再调用panic函数会怎么样呢?
这显然也是不行的,因为,如果在我们调用recover函数时未发生 panic,那么该函数就不会做任何事情,并且只会返回一个nil。
换句话说,这样做毫无意义。那么,到底什么才是正确的recover函数用法呢?这就不得不提到defer语句了。
顾名思义,defer语句就是被用来延迟执行代码的。延迟到什么时候呢?这要延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么。
这与go语句有些类似,一个defer语句总是由一个defer关键字和一个调用表达式组成。
这里存在一些限制,有一些调用表达式是不能出现在这里的,包括:针对 Go 语言内建函数的调用表达式,以及针对unsafe包中的函数的调用表达式。
顺便说一下,对于go语句中的调用表达式,限制也是一样的。另外,在这里被调用的函数可以是有名称的,也可以是匿名的。我们可以把这里的函数叫做defer函数或者延迟函数。注意,被延迟执行的是defer函数,而不是defer语句。
我刚才说了,无论函数结束执行的原因是什么,其中的defer函数调用都会在它即将结束执行的那一刻执行。即使导致它执行结束的原因是一个 panic 也会是这样。正因为如此,我们需要联用defer语句和recover函数调用,才能够恢复一个已经发生的 panic。
我们来看一下经过修正的代码。
package main
import (
"fmt"
"errors"
)
func main() {
fmt.Println("Enter function main.")
defer func(){
fmt.Println("Enter defer function.")
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// 引发panic。
panic(errors.New("something wrong"))
fmt.Println("Exit function main.")
}
在这个main函数中,我先编写了一条defer语句,并在defer函数中调用了recover函数。仅当调用的结果值不为nil时,也就是说只有 panic 确实已发生时,我才会打印一行以“panic:”为前缀的内容。
紧接着,我调用了panic函数,并传入了一个error类型值。这里一定要注意,我们要尽量把defer语句写在函数体的开始处,因为在引发 panic 的语句之后的所有语句,都不会有任何执行机会。
也只有这样,defer函数中的recover函数调用才会拦截,并恢复defer语句所属的函数,及其调用的代码中发生的所有 panic。
至此,我向你展示了两个很典型的recover函数的错误用法,以及一个基本的正确用法。
我希望你能够记住错误用法背后的缘由,同时也希望你能真正地理解联用defer语句和recover函数调用的真谛。
在命令源码文件 demo50.go 中,我把上述三种用法合并在了一段代码中。你可以运行该文件,并体会各种用法所产生的不同效果。
package main
import (
"errors"
"fmt"
)
func main() {
fmt.Println("Enter function main.")
defer func() {
fmt.Println("Enter defer function.")
// recover函数的正确用法。
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// recover函数的错误用法。
fmt.Printf("no panic: %v\n", recover())
// 引发panic。
panic(errors.New("something wrong"))
// recover函数的错误用法。
p := recover()
fmt.Printf("panic: %s\n", p)
fmt.Println("Exit function main.")
}
下面我再来多说一点关于defer语句的事情。
问题 3:如果一个函数中有多条defer语句,那么那几个defer函数调用的执行顺序是怎样的?
如果只用一句话回答的话,那就是:在同一个函数中,defer函数调用的执行顺序与它们分别所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。
当一个函数即将结束执行时,其中的写在最下边的defer函数调用会最先执行,其次是写在它上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一个执行。
如果函数中有一条for语句,并且这条for语句中包含了一条defer语句,那么,显然这条defer语句的执行次数,就取决于for语句的迭代次数。
并且,同一条defer语句每被执行一次,其中的defer函数调用就会产生一次,而且,这些函数调用同样不会被立即执行。
那么问题来了,这条for语句中产生的多个defer函数调用,会以怎样的顺序执行呢?
为了彻底搞清楚,我们需要弄明白defer语句执行时发生的事情。
其实也并不复杂,在defer语句每次执行的时候,Go 语言会把它携带的defer函数及其参数值另行存储到一个链表中。
这个链表与该defer语句所属的函数是对应的,并且,它是先进后出(FILO)的,相当于一个栈。
在需要执行某个函数中的defer函数调用的时候,Go 语言会先拿到对应的链表,然后从该链表中一个一个地取出defer函数及其参数值,并逐个执行调用。
这正是我说“defer函数调用与其所属的defer语句的执行顺序完全相反”的原因了。
下面该你出场了,我在 demo51.go 文件中编写了一个与本问题有关的示例,其中的核心代码很简单,只有几行而已。
package main
import "fmt"
func main() {
defer fmt.Println("first defer")
for i := 0; i < 3; i++ {
defer fmt.Printf("defer in for [%d]\n", i)
}
defer fmt.Println("last defer")
}
总结
我们这两期的内容主要讲了两个函数和一条语句。recover函数专用于恢复 panic,并且调用即恢复。
它在被调用时会返回一个空接口类型的结果值。如果在调用它时并没有 panic 发生,那么这个结果值就会是nil。
而如果被恢复的 panic 是我们通过调用panic函数引发的,那么它返回的结果值就会是我们传给panic函数参数值的副本。
对recover函数的调用只有在defer语句中才能真正起作用。defer语句是被用来延迟执行代码的。
更确切地说,它会让其携带的defer函数的调用延迟执行,并且会延迟到该defer语句所属的函数即将结束执行的那一刻。
在同一个函数中,延迟执行的defer函数调用,会与它们分别所属的defer语句的执行顺序完全相反。还要注意,同一条defer语句每被执行一次,就会产生一个延迟执行的defer函数调用。
这种情况在defer语句与for语句联用时经常出现。这时更要关注for语句中,同一条defer语句产生的多个defer函数调用的实际执行顺序。
以上这些,就是关于 Go 语言中特殊的程序异常,及其处理方式的核心知识。这里边可以衍生出很多面试题目。
思考题
我们可以在defer函数中恢复 panic,那么可以在其中引发 panic 吗?
笔记源码
https://github.com/MingsonZheng/go-core-demo
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
Go语言核心36讲(Go语言进阶技术十六)--学习笔记的更多相关文章
- Go语言核心36讲(Go语言进阶技术十)--学习笔记
16 | go语句及其执行规则(上) 我们已经知道,通道(也就是 channel)类型的值,可以被用来以通讯的方式共享数据.更具体地说,它一般被用来在不同的 goroutine 之间传递数据.那么 g ...
- Go语言核心36讲(Go语言基础知识三)--学习笔记
03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...
- Go语言核心36讲(Go语言实战与应用二)--学习笔记
24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...
- Go语言核心36讲(Go语言进阶技术八)--学习笔记
14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...
- Go语言核心36讲(Go语言进阶技术一)--学习笔记
07 | 数组和切片 我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型. 它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素). 不 ...
- Go语言核心36讲(Go语言进阶技术三)--学习笔记
09 | 字典的操作和约束 至今为止,我们讲过的集合类的高级数据类型都属于针对单一元素的容器. 它们或用连续存储,或用互存指针的方式收纳元素,这里的每个元素都代表了一个从属某一类型的独立值. 我们今天 ...
- Go语言核心36讲(Go语言进阶技术四)--学习笔记
10 | 通道的基本操作 作为 Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学. D ...
- Go语言核心36讲(Go语言进阶技术五)--学习笔记
11 | 通道的高级玩法 我们已经讨论过了通道的基本操作以及背后的规则.今天,我再来讲讲通道的高级玩法. 首先来说说单向通道.我们在说"通道"的时候指的都是双向通道,即:既可以发也 ...
- Go语言核心36讲(Go语言进阶技术六)--学习笔记
12 | 使用函数的正确姿势 在前几期文章中,我们分了几次,把 Go 语言自身提供的,所有集合类的数据类型都讲了一遍,额外还讲了标准库的container包中的几个类型. 在几乎所有主流的编程语言中, ...
随机推荐
- requests接口自动化-数据库参数化
数据库参数化的场景 部分接口,参数其他方式获取不到,可以去数据库去获取,如手机验证码 安装pymysql 配置文件 def sql_conf(): # 定义数据库的配置 host='127.0.0.1 ...
- javascript/html 禁止图片缓存
更新图片, 如果图片的url没有改变, 刷新页面之后图片会使用缓存的图片 Solutions: * js改变图片链接 (添加get参数) // 假设当前这个图片的dom对象为img img.src + ...
- filter_var() 验证邮箱、ip、url的格式 php
验证邮箱格式的正确与否:你的第一解决方案是什么呢? 不管你们怎么思考的:反正我首先想到的就是字符串查找看是否有@符号: 但是对于结尾的.com或者.net 亦或者.cn等等越来越多的域名验证感觉棘手: ...
- 大前端快闪二:react开发模式 一键启动多个服务
最近全权负责了一个前后端分离的web项目,前端使用create-react-app, 后端使用golang做的api服务. npx create-react-app my-app cd my-app ...
- P7099-[yLOI2020]灼【数学期望,结论】
正题 题目链接:https://www.luogu.com.cn/problem/P7099 题目大意 给出\(n\)个坐标轴上的点,\(q\)次询问从某点出发每次等概率向左或者向右一格求到达某个给出 ...
- Windows10 IIS Web服务器安装配置
前言: 对于.NET开发者而已,IIS Web托管服务器应该是十分的熟悉的.对于刚安装Windows10的系统的用户而已Internet Information Services(IIS)功能是默认关 ...
- MySQL学习总结:提问式回顾 undo log 相关知识
原文链接:MySQL学习总结:提问式回顾 undo log 相关知识 1.redo 日志支持恢复重做,那么如果是回滚事务中的操作呢,也会有什么日志支持么? 也回滚已有操作,那么就是想撤销,对应的有撤销 ...
- 羽夏闲谈——VSCode 配置 C/C++环境
前言 VSCode是微软发布一款跨平台的源代码编辑器,其拥有强大的功能和丰富的扩展,使之能适合编写许多语言.我的环境是64位Win10,如果是其他系统请自行判断调整.本人根据网上的配置方法在结合本 ...
- 12种 vo2dto 方法,就 BeanUtils.copyProperties 压测最拉胯!【快双11了,别用错喽】
作者:小傅哥 博客:https://bugstack.cn 原文:https://mp.weixin.qq.com/s/Xq7oQg7dYESMYxHVnxX8Dw 沉淀.分享.成长,让自己和他人都能 ...
- JS 实现计算器功能
括号功能未实现,后续更 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...