21 | panic函数、recover函数以及defer语句 (上)

在本篇,我要给你展示 Go 语言的另外一种错误处理方式。不过,严格来说,它处理的不是错误,而是异常,并且是一种在我们意料之外的程序异常。

前导知识:运行时恐慌 panic

这种程序异常被叫做 panic,我把它翻译为运行时恐慌。其中的“恐慌”二字是由 panic 直译过来的,而之所以前面又加上了“运行时”三个字,是因为这种异常只会在程序运行的时候被抛出来。

我们举个具体的例子来看看。

比如说,一个 Go 程序里有一个切片,它的长度是 5,也就是说该切片中的元素值的索引分别为0、1、2、3、4,但是,我在程序里却想通过索引5访问其中的元素值,显而易见,这样的访问是不正确的。

package main

func main() {
s1 := []int{0, 1, 2, 3, 4}
e5 := s1[5]
_ = e5
}

Go 程序,确切地说是程序内嵌的 Go 语言运行时系统,会在执行到这行代码的时候抛出一个“index out of range”的 panic,用以提示你索引越界了。

当然了,这不仅仅是个提示。当 panic 被抛出之后,如果我们没有在程序里添加任何保护措施的话,程序(或者说代表它的那个进程)就会在打印出 panic 的详细情况(以下简称 panic 详情)之后,终止运行。

现在,就让我们来看一下这样的 panic 详情中都有什么。

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
/Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.go:5 +0x3d
exit status 2

这份详情的第一行是“panic: runtime error: index out of range”。其中的“runtime error”的含义是,这是一个runtime代码包中抛出的 panic。在这个 panic 中,包含了一个runtime.Error接口类型的值。runtime.Error接口内嵌了error接口,并做了一点点扩展,runtime包中有不少它的实现类型。

实际上,此详情中的“panic:”右边的内容,正是这个 panic 包含的runtime.Error类型值的字符串表示形式。

此外,panic 详情中,一般还会包含与它的引发原因有关的 goroutine 的代码执行信息。正如前述详情中的“goroutine 1 [running]”,它表示有一个 ID 为1的 goroutine 在此 panic 被引发的时候正在运行。

注意,这里的 ID 其实并不重要,因为它只是 Go 语言运行时系统内部给予的一个 goroutine 编号,我们在程序中是无法获取和更改的。

我们再看下一行,“main.main()”表明了这个 goroutine 包装的go函数就是命令源码文件中的那个main函数,也就是说这里的 goroutine 正是主 goroutine。再下面的一行,指出的就是这个 goroutine 中的哪一行代码在此 panic 被引发时正在执行。

这包含了此行代码在其所属的源码文件中的行数,以及这个源码文件的绝对路径。这一行最后的+0x3d代表的是:此行代码相对于其所属函数的入口程序计数偏移量。不过,一般情况下它的用处并不大。

最后,“exit status 2”表明我的这个程序是以退出状态码2结束运行的。在大多数操作系统中,只要退出状态码不是0,都意味着程序运行的非正常结束。在 Go 语言中,因 panic 导致程序结束运行的退出状态码一般都会是2。

综上所述,我们从上边的这个 panic 详情可以看出,作为此 panic 的引发根源的代码处于 demo47.go 文件中的第 5 行,同时被包含在main包(也就是命令源码文件所在的代码包)的main函数中。

那么,我的第一个问题也随之而来了。我今天的问题是:从 panic 被引发到程序终止运行的大致过程是什么?

这道题的典型回答是这样的。

我们先说一个大致的过程:某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。

这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。

这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。

随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。

问题解析

panic 可能是我们在无意间(或者说一不小心)引发的,如前文所述的索引越界。这类 panic 是真正的、在我们意料之外的程序异常。不过,除此之外,我们还是可以有意地引发 panic。

Go 语言的内建函数panic是专门用于引发 panic 的。panic函数使程序开发者可以在程序运行期间报告异常。

注意,这与从函数返回错误值的意义是完全不同的。当我们的函数返回一个非nil的错误值时,函数的调用方有权选择不处理,并且不处理的后果往往是不致命的。

这里的“不致命”的意思是,不至于使程序无法提供任何功能(也可以说僵死)或者直接崩溃并终止运行(也就是真死)。

但是,当一个 panic 发生时,如果我们不施加任何保护措施,那么导致的直接后果就是程序崩溃,就像前面描述的那样,这显然是致命的。

为了更清楚地展示答案中描述的过程,我编写了 demo48.go 文件。你可以先查看一下其中的代码,再试着运行它,并体会它打印的内容所代表的含义。

package main

import (
"fmt"
) func main() {
fmt.Println("Enter function main.")
caller1()
fmt.Println("Exit function main.")
} func caller1() {
fmt.Println("Enter function caller1.")
caller2()
fmt.Println("Exit function caller1.")
} func caller2() {
fmt.Println("Enter function caller2.")
s1 := []int{0, 1, 2, 3, 4}
e5 := s1[5]
_ = e5
fmt.Println("Exit function caller2.")
}

我在这里再提示一点。panic 详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。

因此,在针对某个 goroutine 的代码执行信息中,调用栈底端的信息会先出现,然后是上一级调用的信息,以此类推,最后才是此调用栈顶端的信息。

比如,main函数调用了caller1函数,而caller1函数又调用了caller2函数,那么caller2函数中代码的执行信息会先出现,然后是caller1函数中代码的执行信息,最后才是main函数的信息。

goroutine 1 [running]:
main.caller2()
/Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:22 +0x91
main.caller1()
/Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:15 +0x66
main.main()
/Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:9 +0x66
exit status 2

(从 panic 到程序崩溃)

好了,到这里,我相信你已经对 panic 被引发后的程序终止过程有一定的了解了。深入地了解此过程,以及正确地解读 panic 详情应该是我们的必备技能,这在调试 Go 程序或者为 Go 程序排查错误的时候非常重要。

总结

最近的两篇文章,我们是围绕着 panic 函数、recover 函数以及 defer 语句进行的。今天我主要讲了 panic 函数。这个函数是专门被用来引发 panic 的。panic 也可以被称为运行时恐慌,它是一种只能在程序运行期间抛出的程序异常。

Go 语言的运行时系统可能会在程序出现严重错误时自动地抛出 panic,我们在需要时也可以通过调用panic函数引发 panic。但不论怎样,如果不加以处理,panic 就会导致程序崩溃并终止运行。

思考题

一个函数怎样才能把 panic 转化为error类型值,并将其作为函数的结果值返回给调用方?

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言进阶技术十五)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言进阶技术十)--学习笔记

    16 | go语句及其执行规则(上) 我们已经知道,通道(也就是 channel)类型的值,可以被用来以通讯的方式共享数据.更具体地说,它一般被用来在不同的 goroutine 之间传递数据.那么 g ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  3. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  4. Go语言核心36讲(Go语言进阶技术八)--学习笔记

    14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...

  5. Go语言核心36讲(Go语言进阶技术十六)--学习笔记

    22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...

  6. Go语言核心36讲(Go语言进阶技术一)--学习笔记

    07 | 数组和切片 我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型. 它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素). 不 ...

  7. Go语言核心36讲(Go语言进阶技术三)--学习笔记

    09 | 字典的操作和约束 至今为止,我们讲过的集合类的高级数据类型都属于针对单一元素的容器. 它们或用连续存储,或用互存指针的方式收纳元素,这里的每个元素都代表了一个从属某一类型的独立值. 我们今天 ...

  8. Go语言核心36讲(Go语言进阶技术四)--学习笔记

    10 | 通道的基本操作 作为 Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学. D ...

  9. Go语言核心36讲(Go语言进阶技术五)--学习笔记

    11 | 通道的高级玩法 我们已经讨论过了通道的基本操作以及背后的规则.今天,我再来讲讲通道的高级玩法. 首先来说说单向通道.我们在说"通道"的时候指的都是双向通道,即:既可以发也 ...

  10. Go语言核心36讲(Go语言进阶技术六)--学习笔记

    12 | 使用函数的正确姿势 在前几期文章中,我们分了几次,把 Go 语言自身提供的,所有集合类的数据类型都讲了一遍,额外还讲了标准库的container包中的几个类型. 在几乎所有主流的编程语言中, ...

随机推荐

  1. ci框架 自定义配置方法

    系统自动在Application文件夹下生成的config.php文件,采用key-value关联数组的形式来存放配置项和值.为了使结构更清晰,手动新建另外一个配置文件myconfig.php,所采用 ...

  2. 『GoLang』数组与切片

    数组 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构):这种类型可以是任意的原始类型例如整型.字符串或者自定义类型. 数组长度必须是一个常量表达式,并且必须是一个非负 ...

  3. 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 百篇博客分析OpenHarmony源码 | v7.07

    百篇博客系列篇.本篇为: v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...

  4. pytest环境准备与入门

    前言pytest 是 python2 默认自带的,python3 的版本 pytest 框架独立出来了,需用 pip 安装.以下为 python3.7版本为例 pytest是python的一种单元测试 ...

  5. IdentityServer4[5]简化模式

    Implicit简化模式(直接通过浏览器的链接跳转申请令牌) 简化模式是相对于授权码模式而言的.其不再需要[Client]的参与,所有的认证和授权都是通过浏览器来完成的. 创建项目 IdentityS ...

  6. arcgis js4.x在geojson数据上点击显示弹窗,并添加删除按钮

    实例geojsonLayer时添加属性popupTemplate popupTemplate: { title: action, content: '点击了' } 设置title用于查询到多个grap ...

  7. windows terminal+wsl+neovim配置过程杂记

    长期记录,草稿 coc依赖于node,直接sudo apt intsll node得到的版本是10.x,无法满足要求, 这篇博客介绍了安装新版node的方法https://www.cnblogs.co ...

  8. 初探计算机网络之HTTPS请求

    ​ HTTPS自诞生以来,我们总是对它充满着很多的疑问,HTTPS到底是啥?HTTPS多出来的S指的是什么?HTTPS安全可靠吗?访问一个HTTPS的网站的流程等等,带着这些疑问,我们一起来揭开HTT ...

  9. springcloud组件之hystrix服务熔断,降级,限流

    hystrix 简介 Hystrix是什么 在分布式环境中,许多服务依赖项中的一些必然会失败.Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互.Hystrix通过 ...

  10. LOJ6469 Magic(trie)

    纪念我菜的真实的一场模拟赛 首先看到这个题目,一开始就很毒瘤.一定是没有办法直接做的. 我们考虑转化问题 假设,我们选择枚举\(x\),其中\(x\)是\(10\)的若干次方,那么我们只需要求有多少对 ...