声明
本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引起的一些思考。
为什么需要错误和异常处理
任何一行代码都可能存在不可预知的问题,而这些问题就是bug的根源。为了妥善处理这类问题,我们需要编写一些代码,这类代码被称为运维代码。通常情况下,我们需要发现问题、判断问题的种类、然后根据问题的种类,分别进行响应与处理。这些处理可能是写入日志、也可能是直接让代码停止运行,这些都视你的业务逻辑而定。这样一来,在我们编写了足够健壮的运维代码之后,我们便能快速的定位并解决问题。
错误处理方案的演化
单返回值
我们先看一个最原始的错误处理解决方案:Unix读取文件的API:
int open(const char *pathname, int flags);
如果成功打开这个文件,open()会返回一个int类型的文件描述符fd;如果打开失败,便会返回-1。为了正确处理该函数的错误,我们会编写以下代码:
if ((fd = open("/etc/hosts", O_RDONLY)) < 0) {
printf("%s", "open failed")
exit(1);
}
这样做会有什么问题呢?由于错误码和正确的业务逻辑混在一个返回值里,假如你忘了去判断fd的值,代码就会继续往下执行,就会把错误的-1当成正确的fd,代码就会发生不可预知的错误。除此之外,这种错误处理方式的语义也并不清晰。
那么,我们该如何解决这个问题呢?
多返回值
我们想了一下,一个返回值不行,那么搞两个返回值,把错误处理逻辑与正常逻辑区分开,代码逻辑就会变得更加清晰了。Go语言就是这样做的,我们通常能够看到如下代码:
func main() {
res, err := json.Marshal(payload)
if err != nil {
return "", errors.New("序列化请求参数失败")
}
}
通过将正确执行Marshal的返回结果与错误的返回结果分离,使代码语义更加清晰,而且这样会提醒程序员更加关注错误的返回值,而不会忘记进行错误处理。但是,这样仍然存在一个问题,会出现大量的类似代码:
if err != nil {
// 错误处理逻辑
}
在Go语言的代码中,会出现大量对err的if判断逻辑,重复率过高,而且错误处理逻辑仍然和正常的代码逻辑混淆在一起,我们如果想进一步将错误与正常逻辑分离,该如何做呢?
try-catch
Java、PHP等语言提供了try-catch-finally的解决方案。
try {
// 正常代码逻辑
} catch(\Exception $e) {
// 错误处理逻辑
} finally {
// 释放资源逻辑
}
try-catch彻底完成了对错误与正常代码逻辑的分离。我们用try代码块中包裹可能出现问题的代码,在catch中对这些问题代码统一进行错误处理。
资源的释放
finally代码块比较特殊,它被常常用来做一些资源及句柄的释放工作。如果没有finally,我们的代码可能会像这样:
func main() {
mutex := sync.Mutex{
}
// 加锁
mutex