go

go 语言处理错误

在编写程序时,您需要考虑程序可能出现的各种失败情况,并需要处理这些故障。您的用户不需要看到一个漫长而混乱的堆栈跟踪错误。如果他们能看到有意义的关于出错原因的信息,那就更好了。正如您所看到的,Go 语言具有内置函数,如 panic 和 recover,用于管理程序中的异常或意外行为。但是,错误是已知的失败,您的程序应该建立在处理这些错误上。

Go 语言的错误处理方法只是一种控制流程机制,仅需要一个 if 语句和一个 return 语句。例如,当您调用一个函数以从员工对象中获取信息时,您可能想知道员工是否存在。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"
)

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 前缀包含在错误变量中。

最后,当您有一个错误变量时,在调用函数中处理错误时,您可以更具体。error.Is() 函数允许您比较您正在获取的错误类型,例如:

employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

最佳实践

  • 即使您不希望发生错误,请始终检查错误。然后正确处理它们以避免向最终用户公开不必要的信息。
  • 在错误消息中包含前缀,以便您知道错误的来源。例如,您可以包括软件包和函数的名称。
  • 尽可能创建可重用的错误变量。
  • 理解使用返回错误和 panic 的区别。当没有其他选择时,才进行 panic。例如,如果依赖项没有准备好,程序就没有意义(除非您想运行默认行为)。
  • 记录尽可能多的错误详细信息,并打印出最终用户可以理解的错误。

留言

您的电子邮箱地址不会被公开。 必填项已用*标注