最近在学习GoLang,被它很多四不像但又五脏俱全的语言机制弄得一头大雾,其中它的异常机制真的是标新立异,一反现代高级语言 try ... catch的常态,用一套errors panic recover 重新定义了软件开发中的异常处理。本文主要转载自该如何看待 go 语言的异常处理机制,学习一下为什么GoLang会有这样“怪异”的处理方式,及其背后的设计理念


在业务流程的处理过程中,并不是每一个环节必须走通,整个流程才称得上完成。或者说,并不是某个环节出现问题,整个流程就无法完成。比如,在业务处理流程中,需要记录日志。如果日志处理出现问题,是否影响了整个业务流程?再比如,用户下单成功,但是在流水处理环节出现异常,如何处理?

在每一个环节,我们都需要对可预知的问题进行处理,并判断每个环节,对整个业务流程的影响程度。如果影响了主干业务流程,则执行失败。否则,可以忍受某些环节暂时的错误。这些问题可以通过后续的消息重放等手段,对整个业务流程的数据进行补充完整。

在软件编码实现过程中,每一个环节的处理逻辑,需要返回明确的结果以及是否发生错误等信息。这样在组织整个业务流程实现过程中,就可以根据错误信息,来进行不同的处理。主干环节发生异常,则整个业务处理失败。次要环节异常,可进行记录以便后续处理。这样,可以在逻辑层面实现对错误的处理,而非在语言层面实现。

GoLang 之 error

比如在java中,处理异常或者业务规则校验相关的逻辑,这样写:

在java/c#语言中,异常处理都是通过try...catch...finally语言进行处理的。这种方式在go设计者来看是过度使用,其语言运行时需要为函数发生的异常付出太多的资源。如果使用者觉得错误处理很麻烦而忽略错误,那程序会出现不可预知的崩溃。

而从业务层面来看,我们用了Class类、Tuple元组来实现了数据及错误信息的封装,是为了满足业务流程处理的需要,这也是业务系统开发实现所需要的。

结合以上实际问题及需求,go的 Error 机制,看起来是比较好的一种处理方式。允许返回多个值,包括结果及错误信息。

以 net.Dial() 说明,如下

net.Dial() 是Go语言系统包 net 中的一个函数,一般用于创建一个 Socket 连接。
net.Dial 拥有两个返回值,即 Conn 和 error,这个函数是阻塞的,因此在 Socket 操作后,会返回 Conn 连接对象和 error,如果发生错误,error 会告知错误的类型,Conn 会返回空。

GoLang 之 panic

error 错误处理机制,一般是针对可预期的业务、网络、磁盘IO等问题而采用的处理方式(我是这么认为的)。panic 则是对一些不易发现的问题进行处理,比如 数组访问越界、空指针引等问题,包括二方、三方类库直接panic导致的诸多问题。

Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起进程的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。

GoLang 之 recover

虽然我们要谨慎使用 panic 机制,但避免不了经常碰见它,那该如何处理呢?recover 要结合 defer 来使用,放在defer function 内,捕获panic抛出的异常。这种机制只能捕获当前函数以及直接调用的函数可能产生的panic。

go代码示例如下。

如果需要捕获调用过程中,产生的其他协程的panic,则需要语言设计层面进行支持。在c#中,多线程处理是通过一个特殊的异常类AggregateException进行处理的。c#代码示例如下。

知识延伸:
panic 和 recover 可以接受任何类型的值,因为定义为interface{}:
func panic(v interface{})
func recover() interface{}所以工作模式相当于:panic(value)->recover()->value,传递给panic的value最终由recover捕获。另外defer可以配合锁的使用来确保锁的释放,例如:
mu.Lock()
Defer mu.Unlock()
需要注意的是这样会延长锁的释放时间(需要等到函数return)。

总结

defer recover这种机制只是针对当前函数和以及直接调用的函数可能产生的panic,它无法处理其调用产生的其它协程的panic。在java/c#中,不同线程间发生的异常处理,也是捕获不到的。了解操作系统就知道,线程切换是需要系统调度,并保存上下文。如果想要捕获其他线程中的异常,需要语言设计层面的支持。虽然协程是在用户空间,也避免不了这种情况。理论上讲,所有使用协程的地方都必须做defer recover处理,这样才能保证你的应用万无一失,不过开发中可以根据实际情况而定,对于一些不可能出错的函数加了还影响性能。Go的Web服务也是一样,默认的recover机制只能捕获一层,如果你在这个请求的处理中又使用了其它协程,那么必须非常慎重,毕竟只要发生一个panic,整个Web服务就会挂掉。