Golang比Java独特的异常错误处理方式 - Ville

22-03-25 banq

编程语言应该如何对待错误?
大多数语言都使用异常:在这个系统中,被抛出的异常会在调用堆栈中传播,直到它在try-catch块中被处理的那一层。
异常模型将错误视为特殊情况,与程序返回值的常规流程分开处理。
 

try-catch块缺点
这种方法有几个弊端:
首先,它可以向程序员隐藏错误处理路径,特别是在捕捉异常不是强制性的情况下。
比如在Python中。即使在那些有Java风格的必须处理的检查异常的语言中,如果错误的处理方式与原来的调用方式不同,那么错误是从哪里抛出来的也不一定很明显。我们都见过长的代码块被包裹在一个单独的try-catch块中。
在这种情况下,catch块实际上就像一个goto语句,这通常被认为是有害的。

如果你真的从源头上捕捉异常,你会得到一个不太优雅的Go错误模式版本。
这可能会解决混淆代码的问题,但会遇到另一个问题:性能。
在Java等语言中,抛出一个异常可能比从函数中正常返回慢几百倍。
根据这篇文章,Java中最大的性能成本是由打印异常的堆栈跟踪引起的,这很昂贵,因为运行中的程序必须检查它所编译的源代码。
仅仅进入一个try-catach块也不是免费的,因为需要保存CPU的内存寄存器的先前状态,因为在抛出异常的情况下可能需要恢复这些寄存器。

如果你把异常看作是通常不会发生的特殊情况,那么异常的缺点其实并不重要。
对于传统的单体应用程序来说可能是这样的,其中大部分代码库不必进行网络调用--一个操作格式良好的数据的函数不太可能遇到错误(除非是bug的情况下)。
但是只要你在代码中加入I/O,代码无错误的梦想就会破灭:你可以忽略错误,但你不能假装它们不存在!

try {
    doSomething()
} catch (IOException e) {
    // ignore it
}

 

Golang处理错误方式
与其他大多数编程语言不同,Golang接受错误是不可避免的。
如果在单体架构时代还不是这样的话,那么在今天的模块化后端微服务中肯定是这样的,在这种情况下,一个服务往往是外部API调用、对数据库的读写以及与其他服务的通信之间的一个薄的包装。
所有这些都可能失败,解析或验证从它们那里收到的数据(通常是无模式的JSON)也是如此。
Golang将这些调用可能返回的错误明确化,与普通返回值的等级相等。
这是由从函数调用返回多个值的能力所支持的,这在大多数语言中通常是不可能的。
Golang的错误处理系统不仅仅是一个语言上的怪癖,它是一种完全不同的思考错误的方式,替代错误的是返回值。
 

反复出现 if err != nil
对Go的错误处理的一个常见的批评是被迫重复下面的块。

res, err := doSomething()
if err != nil {
 // Handle error
}


对于新用户来说,这可能会觉得毫无用处,是对行数的浪费:一个在其他语言中只需3行的函数很可能会发展到12行。

// In Java
DBResult doSomething() throws IOException {
    APIResult res = getFromAPI()
    Model parsed = parse(res)
    return writeToDB(parsed)
}
// In Golang
func doSomething() error {
    res, err := getFromAPI()
    if err != nil {
        // Handle API error
    }
    parsed, err := parse(res)
    if err != nil {
        // Handle parse error
    }
    res, err = writeToDB(parsed)
    if err != nil {
        // Handle database error
    }
    return res
}


这么多行的代码! 如此低效?

如果你认为上述情况是不优雅或浪费代码,你可能忽略了我们在代码中检查错误的全部原因:我们需要能够以不同的方式处理这些错误 对API或数据库的调用有可能被重试。
有时事件的顺序很重要:在调用外部API之前发生的错误可能不是什么大问题(因为数据从未被发送过去),而在调用API和写到本地数据库之间的错误可能需要立即注意,因为它可能意味着系统最终处于不一致的状态。
即使我们只是想把错误传播给调用者,我们也可能想用对失败原因的解释来包装它们,或者为每个错误返回一个自定义的错误类型。
不是所有的错误都是一样的,向调用者返回适当的错误是API设计的一个重要部分,无论是内部包还是REST API。
你不需要担心在你的代码中重复if err != nil -- Go中的代码就应该是这个样子的。

 

自定义错误类型和错误包装
当从导出的方法中返回错误时,可以考虑指定一个自定义的错误类型,而不是单独使用一个错误字符串。
字符串在意外的代码中是可以的,但在导出的函数中,它们成为函数的公共API的一部分。
改变错误字符串将是一个破坏性的改变--如果没有明确的错误类型,需要检查返回的错误类型的单元测试将不得不依赖于原始的字符串值。
事实上,基于字符串的错误使得在私有方法中测试不同的错误情况也很困难,所以你也应该考虑在包内使用它们。
回到错误与异常的争论上,返回错误也比抛出异常更容易测试代码,因为错误只是一个要检查的返回值。
在你的测试中不需要测试框架或捕捉异常。

更多点击标题