learn-tech/专栏/Go语言核心36讲/21panic函数、recover函数以及defer语句(上).md
2024-10-16 00:01:16 +08:00

111 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
21 panic函数、recover函数以及defer语句 (上)
我在上两篇文章中详细地讲述了Go语言中的错误处理并从两个视角为你总结了错误类型、错误值的处理技巧和设计方式。
在本篇我要给你展示Go语言的另外一种错误处理方式。不过严格来说它处理的不是错误而是异常并且是一种在我们意料之外的程序异常。
前导知识运行时恐慌panic
这种程序异常被叫做panic我把它翻译为运行时恐慌。其中的“恐慌”二字是由panic直译过来的而之所以前面又加上了“运行时”三个字是因为这种异常只会在程序运行的时候被抛出来。
我们举个具体的例子来看看。
比如说一个Go程序里有一个切片它的长度是5也就是说该切片中的元素值的索引分别为0、1、2、3、4但是我在程序里却想通过索引5访问其中的元素值显而易见这样的访问是不正确的。
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文件。你可以先查看一下其中的代码再试着运行它并体会它打印的内容所代表的含义。
我在这里再提示一点。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类型值并将其作为函数的结果值返回给调用方
戳此查看Go语言专栏文章配套详细代码。