了解如何在 Go 中处理错误

已完成

编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panicrecover 之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。

Go 的错误处理方法只是一种只需要 ifreturn 语句的控制流机制。 例如,在调用函数以从 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
}

从现在开始,我们将重点介绍如何修改 getInformationapiCallEmployeemain 函数,以展示如何处理错误。

错误处理策略

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的 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。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。