了解如何在 Go 中处理错误
编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panic
和 recover
之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。
Go 的错误处理方法只是一种只需要 if
和 return
语句的控制流机制。 例如,在调用函数以从 employee
对象获取信息时,可能需要了解该员工是否存在。 Go 处理此类预期错误的一贯方法如下所示:
employee, err := getInformation(1000)
if err != nil {
// Something is wrong. Do something.
}
注意 getInformation
函数返回了 employee
结构,还返回了错误作为第二个值。 该错误可能为 nil
。 如果错误为 nil
,则表示成功。 如果错误不是 nil
,则表示失败。 非 nil
错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。 这是在 Go 中处理错误的方式。 下一部分将介绍一些其他策略。
你可能会注意到,Go 中的错误处理要求你更加关注如何报告和处理错误。 这正是问题的关键。 让我们看一些其他示例,以帮助你更好地了解 Go 的错误处理方法。
我们将使用用于结构的代码片段来练习各种错误处理策略:
package main
import (
"fmt"
"os"
)
type Employee struct {
ID int
FirstName string
LastName string
Address string
}
func main() {
employee, err := getInformation(1001)
if err != nil {
// Something is wrong. Do something.
} else {
fmt.Print(employee)
}
}
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
return employee, err
}
func apiCallEmployee(id int) (*Employee, error) {
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}
从现在开始,我们将重点介绍如何修改 getInformation
、apiCallEmployee
和 main
函数,以展示如何处理错误。
错误处理策略
当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的 getInformation
)可能会将错误返回给调用方,而不执行其他任何操作,如下所示:
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, err // Simply return the error to the caller.
}
return employee, nil
}
你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf()
函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
}
return employee, nil
}
另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟,如下所示:
func getInformation(id int) (*Employee, error) {
for tries := 0; tries < 3; tries++ {
employee, err := apiCallEmployee(1000)
if err == nil {
return employee, nil
}
fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
}
return nil, fmt.Errorf("server has failed to respond to get the employee information")
}
最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。 我们将在下一模块介绍日志记录。 现在,让我们看看如何创建和使用自定义错误。
创建可重用的错误
有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New()
函数创建错误并在若干部分中重复使用这些错误,如下所示:
var ErrNotFound = errors.New("Employee not found!")
func getInformation(id int) (*Employee, error) {
if id != 1001 {
return nil, ErrNotFound
}
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}
getInformation
函数的代码外观更优美,而且如果需要更改错误消息,只需在一个位置更改即可。 另请注意,惯例是为错误变量添加 Err
前缀。
最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is()
函数允许你比较获得的错误的类型,如下所示:
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
fmt.Printf("NOT FOUND: %v\n", err)
} else {
fmt.Print(employee)
}
用于错误处理的推荐做法
在 Go 中处理错误时,请记住下面一些推荐做法:
- 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
- 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
- 创建尽可能多的可重用错误变量。
- 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
- 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。