本文介绍了 Go 语言处理和返回报错的最佳实践。恰当的错误处理可以帮助开发人员更好的理解并调试程序中的问题,报错信息应该描述性的表达出错的原因,并且应该使用错误哨兵和 errors.Is 来更好的实现错误处理和调试。
级别 1: if err != nil
这是最简单的错误返回方法,大多数人都熟悉这种模式。如果代码调用了一个可能返回错误的函数,那么检查错误是否为 nil,如果不是,则返回报错。
这种方法的问题:
虽然这可能是最简单也是最常用的方法,但存在一个主要问题:缺乏上下文。如果代码的调用栈比较深,就没法知道是哪个函数报错。
想象一下,在某个调用栈中,函数 A() 调用 B(),B() 调用 C(),C() 返回一个类似下面这样的错误:
如果运行该程序,将输出以下内容:
我们无法通过报错信息得知调用栈的哪个位置出错,而不得不在代码编辑器中打开程序,搜索特定错误字符串,才能找到报错的源头。
级别 2:封装报错
为了给错误添加上下文,我们用 fmt.Errorf 对错误进行包装。
运行这个程序,会得到以下输出结果:
这样就能知道调用栈。
但仍然存在问题。
这种方法的问题:
我们现在知道哪里报错,但仍然不知道出了什么问题。
级别 3:描述性错误
这个错误描述得不够清楚。为了说明这一点,需要稍微复杂一点的例子。
在本例中,没法通过报错知道是哪个操作失败了,不管是 StepOne 还是 StepTwo,都会收到同样的错误提示:Error:DoSomething: DoSomethingElseWithTwoSteps:UnderlyingError。
要解决这个问题,需要补充上下文,说明具体出了什么问题。
因此,如果 StepOne 失败,就会收到错误信息:DoSomething: DoSomethingElseWithTwoSteps:StepOne failed: UnderlyingError。
这种方法的问题:
- 这些报错通过函数名来输出调用栈,但并不能表达错误的性质,错误应该是描述性的。
- HTTP 状态代码就是个很好的例子。如果收到 404,就说明试图获取的资源不存在。
级别 4:错误哨兵(Error Sentinels)
错误哨兵是可以重复使用的预定义错误常量。
函数失败的原因有很多,但我喜欢将其大致分为 4 类。未找到错误(Not Found Error)、已存在错误(Already Exists Error)、先决条件失败错误(Failed Precondition Error)和内部错误(Internal Error),灵感来自 gRPC 状态码[2]。下面用一句话来解释每种类型。
- Not Found Error(未找到错误):调用者想要的资源不存在。例如:已删除的文章。
- Already Exists Error(已存在错误):调用者创建的资源已存在。例如:同名组织。
- Failed Precondition Error(前提条件失败错误):调用者要执行的操作不符合执行条件或处于不良状态。例如:尝试从余额为 0 的账户中扣款。
- Internal Error(内部错误):不属于上述类别的任何其他错误都属于内部错误。
仅有这些错误类型还不够,必须让调用者知道这是哪种错误,可以通过错误哨兵和 errors.Is 来实现。
假设有一个人们可以获取和更新钱包余额的 REST API,我们看看如何在从数据库获取钱包时使用错误哨兵。
通过下面的 REST 处理程序,可以看到错误哨兵是怎么用的。
再看另一个用户更新余额的例子。
利用哨兵编写更好的错误信息:
我喜欢用以下两种方式来格式化错误信息。
- fmt.Errorf("%w: description: %w", Sentinel, err)
- fmt.Errorf("%w: description", Sentinel)
这样可以确保错误能说明问题,解释出错的现象和根本原因。
这一点很重要,因为从上面的例子中可以看出,同一类型的错误可能是由两个不同的潜在问题造成的。因此,描述可以帮助我们准确找出出错原因。
补充内容:如何记录错误
不需要记录所有错误,为什么?
相反,应该只记录 "被处理" 的错误。所谓的 "被处理" 的错误,是指调用者在收到报错后,可以对错误进行处理并继续执行,而不是仅仅返回错误。
最好的例子还是 REST 处理程序。如果 REST 处理程序收到错误,可以查看错误类型,然后发送带有状态码的响应,并停止传播错误。
参考资料:
- [1] Conquering Errors in Go: A Guide to Returning and Handling errors: https://blog.rideapp.in/conquering-errors-in-go-a-guide-to-returns-and-handling-a13885905433
- [2] gRPC Status Codes: https://grpc.github.io/grpc/core/md_doc_statuscodes.html