Fork me on GitHub

Go语言开发-过程式编程-Panic和Recover

5.5.1. Panic和Recover

Go语言通过其内置的panic()和recover()函数提供异常处理机制。这两个函数提供了可以像其他语言(如C++、Java和Python)一样的通用异常处理机制,但是早Go语言中这样做不是一种好的编程风格。

Go区分错误和异常,错误是指可能出现错误但是程序应该可以处理的事件(例如,文件不能被打开),异常是指不可能发生的事件(例如,一个应该为true的值,在实际中却为false)。

一种处理Go语言中错误的约定成俗的方式是在函数或方法的最后返回一个错误,并在调用的时候检查。(如果将错误打印到控制台,则通常可以忽略返回的错误)。

对于“不可能发生”的情况,我们可以调用内置的panic()函数并传入任意值(例如,传入一个字符串来解释这里出了什么问题)。对于其他语言,我们可以是断言,但在Go语言中,我们调用panic()函数。在程序开发期间或发布之前,最简单的、可能也是最好的方法是调用panic()来中断程序,以强制出现的问题无法被忽略,从而使问题得到修复。但是在我们开始部署程序时,最好避免在发生问题时中断程序,我们可以在可能出现问题的地方调用panic(),然后使用defer recover()来处理。在恢复过程中,我们可以捕获并记录这些panic(以便后续分析),同时返回一个非nil的错误给调用者,这样我们就可以尝试将程序恢复到可以继续安全运行的状态。

当内置的的panic()函数被调用时,正常执行的函数或方法会立即停止执行。然后,defer修饰的函数或方法会被调用,就像函数正常返回一样。最后,控制权会交回给调用者,就像调用的函数或方法调用了panic()一样,这种过程随后在调用者中重复:停止执行,调用defer函数,依此类推。当执行到main ()函数时,main()函数不会被其他函数调用,这是程序会终止执行,并将堆栈跟踪信息输出到os.Stderr,其中包括了调用panic()时传入的值。

刚才描述的是正常情况下当panic发生时其执行逻辑及应如何处理。但是,如果其中一个defer修饰的函数或方法调用了内置的recover()函数(可能仅在一个函数或方法中调用),则panic会中断程序的执行。这时,我们就可以以我们喜欢的方式处理panic。其中一种处理方法是忽视该panic,在这种情况下控制权将会回到含有defer修饰的recover()函数的调用方,然后程序继续正常执行。但是这种方法不建议使用,如果非要如此的话,我们应该至少确保panic会被记录下来。另一种处理方法是,完成必要的清理,然后调用panic()继续抛出异常。一种更常见的处理方法是创建一个error作为函数的返回值,将异常(即,一个panic())转换成一个错误(即,一个error)。

在几乎所有情况下,Go语言标准库都优先使用error而不是panic。对于我们的自定义包来说,最好不要使用panic();或者,如果必须要用的话,请使用recover()来捕获panic()并返回error,就像标准库那样。

其中一个例子是Go语言中基本的正则表达式包regexp,该包含有几个用于创建正则表达式的函数,包括regexp.Compile() 和regexp.MustCompile()。前一个函数返回一个编译好的正则表达式和nil,如果传递给它的字符串不是有效的正则表达式则返回nil和error。第二个函数要么返回一个编译好的正则表达式,要么抛出异常。前一个函数多用于当正则表达式来自外部源时(例如,用户输入或从文件中读取)。如果要把正则表达式硬编码到程序中,最好使用第二个函数,因为它可以确保当程序运行时,如果正则表达式错误,程序将因异常而中断执行。

我们应该在什么时候允许异常中断程序执行,什么时候应该使用recover()?这是两个必须要考虑的问题。作为开发者,我们希望在出现逻辑错误时,程序应该尽快的被中断,以便我们识别并修复这些问题,但我们不希望的是等程序部署后才出问题。

对于那些仅通过运行程序就可以捕获的错误(例如,无效的正则表达式),我们应该使用panic()(或有panic功能的函数如regexp.MustCompile()),因为我们永远不应该部署那些运行后立即崩溃的应用程序。我们必须小心谨慎,只在确定会被调用的函数中这样做,例如,main包中的init()函数(如果有的话)和main()函数,以及导入的自定义包中的任意init()函数,当然,还有这些函数调用的任何其他函数或方法。如果我们使用了单元测试,在单元测试中所引用的函数或方法也应该这样做。自然地,我们还必须确保,无论程序的控制流程如何,这种潜在的异常也应该被处理。

对于那些在特定环境下运行过程中可能被调用也可能不被调用的函数或方法,如果我们调用了panic(),就应该同时使用recover(),或者调用的函数或方法出现了异常,则应该将异常转换为错误。理想情况下,recover()应该紧跟着panic(),并在尽可能和适当的情况下,在函数结束或返回错误之前将程序恢复到可以继续运行的状态。对于mian包中的main()函数,我们可以将recover()放在main()函数的开始,以便记录所有的异常,但不幸的是,程序将在recover()处理完异常后中断运行,这是一种临时解决方案,稍后我们将会用到。

接下来我们将介绍两个例子,第一个演示了如何将异常转换为错误,第二个演示了如何使程序更健壮。

设想一下这种情况,我们使用了某一个包,有一个函数位于包的深处,但我们不能修改该函数,因为它来自我们无法控制的第三方。

上面的函数可以安全地将int64转换为int类型,如果转换错误,则抛出异常。

为什么这样的函数一开始就会使用panic()?我们通常希望程序在出现问题时立即强制崩溃, 以便尽早处理编程错误。另一种情况是,函数调用一个或多个函数,被调用的函数又调用其他函数,如果出了问题,我们希望控制权能立即递交给最初的函数,因此,如果调用的函数出了问题就抛出异常,我们就使用recover()来捕获这些异常(无论来自哪里)。通常情况下,我们希望包里的函数将问题报告为错误而不是异常,因此在包中使用panic()和recover()以确保异常不会被泄漏并被转换为错误是相当常见的。另一种使用情况是,将panic(“unreachable”)的调用放在程序逻辑中无法达到的地方(例如,具有return语句的函数的末尾),或者当前置或后置条件被破坏时调用panic()函数。这样做可以确保,如果我们破坏了函数的逻辑,我们很快就会知道并可以及时处理。

如果上述原因都不适用,那么我们应该避免产生异常,并在出现问题时返回一个非nil的错误值。因此,在下面的例子中,如果转换成功,则返回一个int和nil值;如果转换失败,则返回一个int值和一个错误。

通常情况下,Go语言会自动将返回值设置为它们所在类型的零值,所以如果上面的函数被调用,则返回0和nil。如果自定义函数ConvertInt64ToInt()的正常返回,则其结果会被赋值给i并返回该值,并同时返回一个nil,以表示没有错误发生。但是,如果ConvertInt64ToInt()函数产生了异常,该异常会在defer修饰的匿名函数中被捕获处理,这里将错误的文本形式赋值给err变量。

正如IntFromInt64()函数所演示的,将异常转换为错误是简单易行的。

对于我们的第二个例子,我们将考虑如何使web服务器在遇到异常时更加健壮。请回顾一下第2章的Statistics例子。如果我们遇到程序错误,例如,我们不小心将nil作为image.Image值传递给函数,这时程序就会产生异常,如果没有recover()函数处理,程序将会被中断执行。当然,如果网站对我们很重要,且我们希望该网站在无人看管的情况下一直能够正常运行,那这种情况是令人很难接受的。我们所期望的是即使有异常,程序也应该继续运行,我们应该将这些异常记录下来,以便在空闲时修复它们。

我们创建了一个statistics例子的更新版本(实际上是statistics_ans解决方案的更新版本),在statistics_nonstop/statistics.go文件内。其中一个修改是在页面上添加一个测试用的Panic!按钮,该按钮可以点击并可以产生一个异常,当然更重要的是这可以让服务在发生异常时可以继续运行,同时可以帮助我们理解发生了什么事情,我们还记录了客户端何时正常响应,服务端何时收到错误请求以及服务端是否被重启。下面是一个典型的日志输出。

为了让日志更简单易读,我们使用了log包但不打印时间戳。

在查看我们所做的更改之前,让我们简要回顾一下原来的代码。

这个网站只有一个页面,尽管我们现在使用的技术可以很容易地应用于具有多个页面的网站。如果这里产生了异常,且没有recover()来捕获处理的话,异常就会上抛至main()函数,该网站将中断运行,这就是我们必须要防范的。

为了使web服务在遇到异常时更加健壮,我们必须确保每个页面处理函数都具有一个defer修饰的调用recover()的匿名函数。

这样做可以阻止异常抛出至上层函数,然而,这并不能阻止页面处理程序返回(因为defer语句是在函数返回之前执行的),但这并不重要,因为不论何时有页面请求,http.ListenAndServe()函数就会重新调用页面处理程序。

当然,对于一个拥有大量页面处理程序的大型网站来说,添加defer函数来捕获和记录异常会涉及大量的重复代码,并且很容易遗漏一些页面。我们可以通过创建一个具有每个页面处理程序所需代码的封装函数来解决。使用封装函数,我们只需要修改http.HandleFunc()函数即可。

这里我们原有的homePage()函数(即,没有调用recover()的defer函数),依靠logPanics()封装函数来处理异常。

上面的函数接收一个HTTP处理程序函数作为其参数,创建并返回一个匿名函数,该匿名函数含有一个defer修饰的用于捕获和记录异常的匿名函数,同时调用传入的处理程序函数。这与将defer函数添加至homePage()函数效果一样,但更便捷,因为我们不必将defer函数添加到所有的页面处理程序中;相反,我们使用logPanics()封装函数将每一个页面处理函数传入到http.HandleFunc()函数中。

使用此方案的statistics程序位于文件statistics_nonstop2/statistics.go中。下面关于闭包的小节将介绍匿名函数。


目录


作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注