错误处理是编程语言中很重要的组成部分。一般来说,发生错误时,要立即中止程序正常逻辑的执行,转而执行错误处理逻辑,这个过程称为错误处理。 我用过的编程语言中,比较熟悉的两种错误处理方式,一种是异常抛出,一种是错误返回。它们各有优缺点,也有各自胜任的场景。
先来看看它们各自是怎么处理错误的。
以 Python 为例,抛出异常的方式是:
def foo():
# do something
raise Exception("something wrong")
处理异常的方式是:
try:
foo()
except Exception as e:
# handle exception
以 Go 为例,返回错误的方式是:
func foo() (int, error) {
// do something
return 0, errors.New("something wrong")
}
处理错误的方式是:
value, err := foo()
if err != nil {
// handle error
}
看上去它们完成的事情差不多,但如果我们去掉错误处理的代码,不管它,会变成这样:
Python:
foo()
Go:
value, _ := foo()
两者造成的结果截然不同,Python 会上报异常,Go 会忽略错误。这代表了两种不同的哲学,前者若不处理错误即上报异常,让上层处理,而后者若不处理错误,则继续执行。
似乎异常抛出的方式比较好,然而这种方式,应用在动态语言上,就出问题了,调用者不知道调用的这段代码会不会报错,报什么错,这就导致程序永远会在无法预料的情况下崩溃。恰巧,现在两种主要的动态语言,Python 和 Javascript,都采用的这种方式。而一些开发者,为了保住 SLO 和 KPI,就会用 try <一大坨> except:pass
的代码兜底。 底看似兜住了,其实早已千疮百孔。
这不是抛出异常的错,这是动态语言的问题,Java 也是用第一种异常抛出的方式,但由于它有完善的异常标注和静态检查,异常也不会随意泄漏导致程序崩溃。
相反,用 Go 语言的时候,你一看它返回了一个 err
,脑子就永远有根弦,要么必须写 if err != nil
,要么主动用 _
忽略掉错误,采用任何一种方式,就算是再粗心的程序员,都清晰地知道自己在做什么,反而更有利于及时的处理错误。 写 Go 的时候感觉自己一直在 if err != nil
正是因为每一个错误都被兜住了,不会漏掉。但尴尬的是,不是所有错误在本函数中都能处理,对于无法处理的错误,只能把错误返回给上层,而上层也不一定能处理,于是就一直 return。一个例子是用户交互程序, 你需要把一些关键错误信息显示在界面上,而这个错误的来源,可能是任意层级深度的,这时异常抛出的「直达天听」的优势就显现出来了。
至于 Rust 的 Result<T, E>
类型,本质上也是返回错误,它除了有一堆 map, map_err, unwrap, unwrap_or_else
等方法方便人使用,还有 ?
运算符可以立即返回错误。比起 if err != nil { return err }
来说,方便了太多。 但谁让 Golang 是大道至简,去掉这些糖,Rust 和 Go 的错误处理方式其实是一样的。
总结,我认为异常抛出的方式,总体上是更省事的,你不知道怎么处理这个错误的时候就不处理,让上层去处理。而返回错误的方式,特别是在语言层面没有提供语法糖的时候,你就必须要处理错误。 但异常抛出的方式应用在动态语言上很容易造成错误的泄漏,这些语言可能反而会比较适合返回错误的方式。