在编写 Go 代码的时候,有个东西让我很挫败,那就是 Go 语言中的 panic。可能是因为我对它的认识还不够,因为从以往的代码经验来说,C++、Java、Python 中的异常和错误处理都是比较类似的,可以用 try-catch
逻辑操作,但是 Go 中的 panic 我一开始认为更像 Linux 中的系统函数 exit
。随着对 Go 代码的不断读写,对 Go 中的一些常见的错误和异常处理有了一些常识,所以本文我决定尝试对这两个东西做一些不深不浅的介绍。
错误和异常的区别是啥
首先,要说一下的就是 Go 语言中的错误和异常和在 Java 中的错误和异常有点一样,在 Java 中,错误一般就是没得救了,例如 JVM 内存爆炸之类的;然后,异常会分为运行时异常和非运行时异常,我觉得这里更像是 Go 中的异常和错误。以 Java 为参照来说,我的认识是 Go 所谓的错误其实就是代码层面可感知,可防御的,例如参数不合法,资源不存在之类的业务异常,所以常见的做法就是函数带上一个 error
返回值,而且,惯例是 error
作为返回值的最后一位;至于异常,那就是没得防御的,例如下标溢出,除数为 0 这种,类似于 Java 中的运行时错误,在 Go 中一般都是通过 panic
来处理,当然,你要强行 return error
也没人拦你,所以这里就区分了错误和异常的边界。
Go 的异常处理
Go 的异常处理和其他我用过的语言中的 try/catch
组合不太一样,据说是为了代码的整洁性,然而,事实上我的使用体验是并没有整洁太多,不过清晰度来说,倒是会更清晰一下,毕竟不用一个函数里面好几个 try/catch
了,那么 Go 是怎么操作的呢?
Go 中对于异常的处理其实就是三个关键词操作的,分别是 defer
、panic
和 recover
,下面就来说说这三个关键字怎么用:
defer
:放在函数里头,一个函数里面可以有多个defer
,按照定义顺序先定义后执行的原则,在函数返回前执行;panic
:函数从调用的地方中断,将panic
的调用参数入栈,函数准备返回,如果函数有defer
会先执行defer
;recover
:定义在defer
函数的第一层(只在第一层生效),当defer
所在的函数有panic
时,recover
会返回panic
的参数,并且消化掉,从而让defer
所在的函数不会以panic
的形式返回。
这样说好像有点蒙圈,下面还是上实例吧,有两个示例,先看第一个:
你可以尝试先猜测一下这段代码的输出是什么,然后对比一下真实运行结果:
main
stack b
fault
stack a
这是很合理的,根据 Go 语言的规则,先执行主逻辑,Line 7 和 Line 10 的 defer
将延后执行,所以先输出的是 main,然后因为 panic
了,所以按照先定义后执行的规则,Line 10 的 defer
先被执行,所以先输出了 stack b
,然后因为这里调用了 recover
所以 panic
的参数被捕获了,在 Line 13 被打印出来了;接着就是 stack a
了。
这整个逻辑就是这么简单,然后需要注意的是,这里就有点类似于我们其他语言中的这个逻辑:
是不是觉得 Go 的虽然没减少多少复杂度,但是清晰度上可能会更舒服一些?
这里有几个我觉得有必要注意的点:
panic
如果不recover
的话,会一直沿着调用链panic
上去,直到被捕获或者程序退出;panic
可以在任何地方引发,但recover
函数只有在defer
函数中被直接调用的时候才可以获取panic
的参数;- 如果在
defer
中使用了recover
函数,则会捕获错误信息,使该错误信息终止报告。
所以这里有个问题,那就是如果我用 recover
捕获异常之后,万一不是我关注的异常怎么办,我要怎么做一个撤销的操作?很遗憾,其实我一直没有发现更好的方式,一个可用的方式就是再次 panic
,除非你在 defer
里面还定义了 defer
不然,你所捕获的异常可以再次被抛出,只不过会多了两层调用关系。下面来看个示例:
这里的 Line 13 就做了一个再次抛出异常的操作,这样函数外部就可以根据自己的需要进行异常处理。
异常的管理
既然 Go 支持异常的捕获和抛出,那么我们应该如何知道什么时候该捕获什么时候该让异常终止程序呢?就我个人而言,对于自己编写的代码逻辑,基本上都用不到异常捕获,因为大部分场景都用错误返回值代替了,如果真的需要用到异常,那么很可能是传递了可能导致程序崩溃的参数或者配置,而且这个是我不可控的或者是我不希望控制的;
另外一个我认为需要捕获异常的场景就是使用了其他人的库,可能是其他同事的,也可能是第三方的或者开源的,因为我们无法控制库的行为,所以只能根据库的文档和实现来做响应的适应。
这里需要强调的是,并不是将所有的异常都转化为错误是一个好的实践,因为就我个人的感受而言,这带给我一个非常大的困扰就是你会发现代码中有大量类似于这样的逻辑:
if _, err := doSomethingInteresting(); err != nil {
// something to do
return err
}
这就很恶心了,没有经过科学的统计,但凭直观来说,基本上代码翻个一页肯定可以看到类似于 err 的处理,所以这就要求在写代码的时候,对于错误和异常的界定和处理是一个很考验能力的地方,也是我需要努力的一个地方。