在编写 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 中对于异常的处理其实就是三个关键词操作的,分别是 deferpanicrecover,下面就来说说这三个关键字怎么用:

这样说好像有点蒙圈,下面还是上实例吧,有两个示例,先看第一个:

你可以尝试先猜测一下这段代码的输出是什么,然后对比一下真实运行结果:

main
stack b
fault
stack a

这是很合理的,根据 Go 语言的规则,先执行主逻辑,Line 7Line 10defer 将延后执行,所以先输出的是 main,然后因为 panic 了,所以按照先定义后执行的规则,Line 10defer 先被执行,所以先输出了 stack b,然后因为这里调用了 recover 所以 panic 的参数被捕获了,在 Line 13 被打印出来了;接着就是 stack a 了。

这整个逻辑就是这么简单,然后需要注意的是,这里就有点类似于我们其他语言中的这个逻辑:

是不是觉得 Go 的虽然没减少多少复杂度,但是清晰度上可能会更舒服一些?

这里有几个我觉得有必要注意的点:

所以这里有个问题,那就是如果我用 recover 捕获异常之后,万一不是我关注的异常怎么办,我要怎么做一个撤销的操作?很遗憾,其实我一直没有发现更好的方式,一个可用的方式就是再次 panic,除非你在 defer 里面还定义了 defer 不然,你所捕获的异常可以再次被抛出,只不过会多了两层调用关系。下面来看个示例:

这里的 Line 13 就做了一个再次抛出异常的操作,这样函数外部就可以根据自己的需要进行异常处理。

异常的管理

既然 Go 支持异常的捕获和抛出,那么我们应该如何知道什么时候该捕获什么时候该让异常终止程序呢?就我个人而言,对于自己编写的代码逻辑,基本上都用不到异常捕获,因为大部分场景都用错误返回值代替了,如果真的需要用到异常,那么很可能是传递了可能导致程序崩溃的参数或者配置,而且这个是我不可控的或者是我不希望控制的;

另外一个我认为需要捕获异常的场景就是使用了其他人的库,可能是其他同事的,也可能是第三方的或者开源的,因为我们无法控制库的行为,所以只能根据库的文档和实现来做响应的适应。

这里需要强调的是,并不是将所有的异常都转化为错误是一个好的实践,因为就我个人的感受而言,这带给我一个非常大的困扰就是你会发现代码中有大量类似于这样的逻辑:

if _, err := doSomethingInteresting(); err != nil {
    // something to do
    return err
}

这就很恶心了,没有经过科学的统计,但凭直观来说,基本上代码翻个一页肯定可以看到类似于 err 的处理,所以这就要求在写代码的时候,对于错误和异常的界定和处理是一个很考验能力的地方,也是我需要努力的一个地方。

Reference

  1. The Go Blog, Defer, Panic, and Recover
  2. On the uses and misuses of panics in Go
  3. Golang错误和异常处理的正确姿势
  4. Go 语言的错误处理机制是一个优秀的设计吗?
  5. Golang: 深入理解panic and recover
  6. Java 中的运行时异常和非运行时异常