什么是Panic?
在Go程序中,处理异常情况的惯用方法是使用errors
。对于程序中出现的大多数异常情况,errors已经足够了。
但有些情况下,程序在出现异常情况后无法继续执行。在这种情况下,我们使用panic来过早地终止程序。当一个函数遇到panic时,它的执行被停止,任何defer
函数被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回为止,这时程序会打印出panic
信息,然后是堆栈跟踪,然后终止。当我们写一个例子程序时,这个概念就会更清楚。
使用recover
可以重新获得对panic
程序的控制,我们将在本教程的后面讨论。
panic和recover可以说是类似于其他语言(如Java)中的try-catch-finally语法,只是在Go中很少使用。
什么时候应该使用panic?
一个重要的因素是,你应该避免panic
和recover
,尽可能地使用errors。只有在程序无法继续执行的情况下,才应该使用panic和recover机制。
panic有两种有效的使用情况。
- 一个无法恢复的错误,程序不能简单地继续执行。
一个例子是一个网络服务器无法绑定到所需的端口。在这种情况下,panic是合理的,因为如果端口绑定本身失败了,就没有其他事情可做。 - 一个程序员的错误。
假设我们有一个接受指针作为参数的方法,有人用一个nil
参数调用这个方法。在这种情况下,我们可以panic,因为用nil
参数调用一个方法是一个程序员的错误,而这个方法期望的是一个有效的指针。
panic实例
下面提供了内置panic
函数的签名。
1 | func panic(interface{}) |
当程序终止时,传递给panic函数的参数将被打印出来。当我们写一个示例程序时,它的用途就会很清楚。所以我们马上就来做这件事。
我们将从一个假想的例子开始,它展示了panic是如何工作的。
1 | package main |
以上是一个简单的程序,用于打印一个人的全名。第7行中的fullName
函数可以打印一个人的全名。该函数在第8行和第11行分别检查firstName
和lastName
指针是否为nil
。如果为nil
,该函数将调用panic
,并给出相应的信息。这个信息将在程序终止时被打印出来。
运行这个程序将打印出以下输出。
1 | panic: runtime error: last name cannot be nil |
让我们分析一下这个输出,以了解panic是如何工作的,以及当程序发生panic时,堆栈跟踪是如何打印的。
在第19行,我们将Elon分配给firstName
。在第20行中,我们调用fullName
函数,而lastName
为nil
。因此,第11行的条件满足,程序将出现panic。当遇到panic时,程序执行终止,传递给panic函数的参数被打印出来,然后是堆栈跟踪。由于程序在第12行调用panic函数后终止,第13、14和15行的代码将不会被执行。
这个程序首先打印了传递给panic
函数的信息。
1 | panic: runtime error: last name cannot be nil |
然后打印出堆栈跟踪。
程序在fullName
函数的第12行慌了手脚,因此
1 | goroutine 1 [running]: |
将首先被打印出来。然后,堆栈中的下一个项目将被打印出来。在我们的例子中,main
函数第20行调用fullName
的是堆栈跟踪中的下一个项目。因此,它将被打印出来。
1 | main.main() |
现在我们已经到达了引起panic的顶层函数,上面已经没有更多的层次了,因此没有什么可打印的了。
再举一个例子
panic也可以由运行时发生的错误引起,比如试图访问一个不存在于切片中的索引。
让我们写一个特意设计的例子,由于超界的切片访问而产生panic。
1 | package main |
在上面的程序中,在第9行,我们试图访问n[4]
,而这是切片中的无效索引。这个程序会出现panic,输出结果如下:
1 | panic: runtime error: index out of range [4] with length 3 |
panic期间的defer调用
让我们回忆一下panic的作用。当一个函数遇到panic时,它的执行被停止,任何延迟的函数被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回为止,这时程序会打印出panic信息,然后是堆栈跟踪,然后终止。
在上面的例子中,我们没有推迟任何函数调用。如果有一个defer
函数调用,它就会被执行,然后控制权再返回给它的调用者。
让我们稍微修改一下上面的例子,使用一个defer语句。
1 | package main |
唯一的变化是在第8行和第20行增加了延迟的函数调用。
这个程序会打印出来。
1 | deferred call in fullName |
当程序在第13行出现panic时,任何defer
函数调用都会首先被执行,然后控制权返回其上层调用者,以此类推,直到到达最高级别的调用者。
在我们的例子中,fullName
函数第8行中的defer
语句首先被执行,这将打印出以下信息:
1 | deferred call in fullName |
然后控制权返回到main
函数,main
函数的延迟调用被执行,因此会打印出以下信息:
1 | deferred call in main |
现在控制权已经到达顶层函数,因此程序会打印出panic信息和堆栈跟踪,然后终止。
从panic中recover
recover是一个内置函数,用于重新获得对panic程序的控制。
以下是recover函数的签名。
1 | func recover() interface{} |
recover函数只有在延迟函数内调用时才有用。在defer
函数内部执行对recover的调用,通过恢复正常执行来停止panic序列,并检索传递给panic函数的errors信息。如果recover在defer
函数之外被调用,它将不会停止panic序列。
让我们修改我们的程序,使用recover来恢复panic后的正常执行。
1 | package main |
第7行的recoverFullName()
函数调用recover()
,该函数返回传递给panic
函数的值。这里我们只是在第9行打印recover()
返回的值。recoverFullName()
在fullName
函数的第14行中被推迟了。
当fullName
发生panic时,defer
函数recoverName()
将被调用,它使用recover()
来停止panic。
这个程序会打印出来。
1 | recovered from runtime error: last name cannot be nil |
当程序在第19行panic时,延迟的recoverFullName()
将被调用,该函数反过来调用recover()
以重新获得对panic的控制。第8行对recover()
的调用将返回传递给panic的参数,因此它打印出来。
1 | recovered from runtime error: last name cannot be nil |
在执行recover()
后,panic停止,控制权返回到上级调用者,在这里是指main
函数。由于panic已经被恢复了,程序从main
的第29行开始继续正常执行😃。它打印先打印returned normally from main
再打印deferred call in main
。
让我们再看一个例子,我们从访问切片的无效索引引起的panic中恢复。
1 | package main |
运行上述程序将输出。
1 | Recovered runtime error: index out of range [4] with length 3 |
从输出结果来看,你可以了解到我们已经从panic中恢复过来了。
recover后获取堆栈跟踪
如果我们从panic中恢复,我们会失去关于panic的堆栈跟踪。甚至在上面的程序恢复后,我们也失去了堆栈跟踪。
有一种方法可以使用Debug包的PrintStack函数来打印堆栈记录
1 | package main |
在上面的程序中,我们在第11行使用debug.PrintStack()
来打印堆栈跟踪。
这个程序将打印。
1 | recovered from runtime error: last name cannot be nil |
从输出中,你可以了解到,panic被恢复了,并且recovered from runtime error: last name cannot be nil
被打印出来。在这之后,堆栈跟踪被打印出来。然后
1 | returned normally from main |
是在panic恢复后打印的。
panic、recover和Goroutines
只有当它从发生panic的同一个goroutine中调用时,recover才起作用。不可能从发生在不同goroutine的panic中recover。让我们通过一个例子来理解这一点。
1 | package main |
在上面的程序中,函数divide()
将在第22行发生panic,因为b是0,而一个数字不可能被0除。sum()
函数调用了一个延迟函数recovery()
,用于从panic恢复。函数divide()
在第17行作为一个单独的goroutine被调用。我们在第18行中等待已完成的通道,以确保divide()
完成执行。
你认为该程序的输出是什么?panic是否会被恢复?答案是否定的。panic将不会被恢复。这是因为recover
函数存在于不同的goroutine中,而panic发生在不同goroutine的divide()
函数中。因此,recover是不可能的。
运行这个程序将打印。
1 | 5 + 0 = 5 |
你可以从输出中看到,recover并没有发生。
如果在同一个goroutine中调用divide()
函数,我们就会从panic中恢复。
如果将程序的第17行的
1 | go divide(a, b, done) |
改为
1 | divide(a, b, done) |
就会发生recover,因为panic发生在同一个goroutine中。如果程序运行时有上述改变,它将打印出
1 | 5 + 0 = 5 |
这样我们就到了本教程的结尾。
下面是对本教程中所学内容的简要回顾。
- 什么是Panic?
- 什么时候应该使用Panic?
- panic的使用实例
- panic期间defer函数
- 从panic中recover
- recover后获得堆栈跟踪
- panic、recover和Goroutines