处理Go中的错误

强大的代码需要对错误的用户输入,错误的网络连接和故障磁盘等意外情况做出正确反应。错误处理是识别程序何时处于意外状态的过程,并采取措施记录诊断信息以供以后调试。

强大的代码需要对错误的用户输入,错误的网络连接和故障磁盘等意外情况做出正确反应。 错误处理是识别程序何时处于意外状态的过程,并采取措施记录诊断信息以供以后调试。

与需要开发人员使用专门语法处理错误的其他语言不同,Go中的错误是具有从任何其他值的函数返回的类型error值。 要处理Go中的错误,我们必须检查函数可能返回的这些错误,确定是否发生了错误,并采取适当的措施来保护数据并告诉用户或操作员发生了错误。

创建错误

在我们处理错误之前,我们需要先创建一些错误。 标准库提供了两个内置函数来创建错误: errors.Newfmt.Errorf 这两个功能都允许您指定稍后可以向用户显示的自定义错误消息。

errors.New采用单个参数 - 一个错误消息作为字符串,您可以自定义以提醒用户出错的地方。

尝试运行以下示例以查看由errors.New打印到标准输出的错误:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("barnacles")
    fmt.Println("Sammy says:", err)
}
Sammy says: barnacles

我们使用了标准库中的errors.New函数来创建一个新的错误消息,其中包含字符串"barnacles"作为错误消息。 我们在这里遵循惯例,使用小写作为错误消息,如Go编程语言风格指南所示。

最后,我们使用fmt.Println函数将我们的错误消息与"Sammy says:"结合起来。

fmt.Errorf函数允许您动态生成错误消息。 它的第一个参数是一个字符串,其中包含带有占位符值的错误消息,例如字符串的%s和整数的%d fmt.Errorf按顺序将此格式字符串fmt.Errorf的参数插入到这些占位符中:

package main

import (
    "fmt"
    "time"
)

func main() {
    err := fmt.Errorf("error occurred at: %v", time.Now())
    fmt.Println("An error happened:", err)
}
An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

我们使用fmt.Errorf函数来构建包含当前时间的错误消息。 我们提供给fmt.Errorf的格式化字符串包含%v格式化指令,该指令告诉fmt.Errorf使用格式化字符串后提供的第一个参数的默认格式。 该参数将是当前时间,由标准库中的time.Now函数提供。 与前面的示例类似,我们将错误消息与短前缀结合使用, fmt.Println使用fmt.Println函数将结果打印到标准输出。

处理错误

通常,您不会看到像这样创建的错误,不能立即用于其他目的,如上例所示。 在实践中,创建错误并在出现问题时从函数返回错误更为常见。 然后,该函数的调用者将使用if语句来查看错误是否存在或nil -an未初始化的值。

下一个示例包含始终返回错误的函数。 请注意,当您运行程序时,它产生的输出与前一个示例相同,即使函数此次返回错误。 在其他位置声明错误不会更改错误消息。

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    err := boom()

    if err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
An error occurred: barnacles

这里我们定义一个名为boom()的函数,它返回我们使用errors.New构造的单个error 。新的。 然后我们调用此函数并使用行err := boom()捕获错误。
一旦我们分配了这个错误,我们检查是否存在if err != nil条件。 这里条件总是评估为true ,因为我们总是从boom()返回一个error

情况并非总是如此,因此最好使用不存在错误的逻辑处理案例( nil )和存在错误的情况。 当出现错误时,我们使用fmt.Println打印我们的错误以及前缀,就像我们在前面的示例中所做的那样。 最后,我们使用return语句来跳过fmt.Println("Anchors away!")执行,因为这应该只在没有错误发生时执行。

注意:上一个示例中显示的if err != nil构造是Go编程语言中错误处理的主力。 只要函数可以产生错误,使用if语句检查是否发生了错误就很重要。 通过这种方式,惯用的Go代码自然地在第一个缩进级别具有其“快乐路径”逻辑,并且在第二个缩进级别具有所有“悲伤路径”逻辑。

如果语句有一个可选的赋值子句,可用于帮助压缩调用函数和处理其错误。

运行下一个程序以查看与前面示例相同的输出,但这次使用复合if语句来减少一些样板:

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    if err := boom(); err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
An error occurred: barnacles

和以前一样,我们有一个函数boom() ,它总是返回一个错误。 我们将从boom()返回的错误分配给err作为if语句的第一部分。 if语句的第二部分中,在分号后面,该err变量随后可用。 我们检查是否存在错误并使用短前缀字符串打印我们的错误,就像我们之前所做的那样。

在本节中,我们学习了如何处理仅返回错误的函数。 这些函数很常见,但能够处理可返回多个值的函数的错误也很重要。

与价值观一起回归错误

返回单个错误值的函数通常是那些影响某些状态更改的函数,例如将行插入数据库。 如果函数成功完成,那么编写返回值的函数也常见,如果函数失败则返回潜在错误。 Go允许函数返回多个结果,可用于同时返回值和错误类型。

要创建一个返回多个值的函数,我们列出函数签名中括号内每个返回值的类型。 例如,返回stringerrorcapitalize函数将使用func capitalize(name string) (string, error) {} (string, error)部分告诉Go编译器该函数将按此顺序返回stringerror

运行以下程序以查看返回stringerror的函数的输出:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    name, err := capitalize("sammy")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }

    fmt.Println("Capitalized name:", name)
}
Capitalized name: SAMMY

我们将capitalize()定义为一个函数,它接受一个字符串(名称为大写)并返回一个字符串和一个错误值。 main() ,我们调用capitalize()并将函数返回的两个值分配给nameerr变量,方法是在:=运算符的左侧用逗号分隔它们。 在此之后,我们执行if err != nil检查,如前面的示例所示,如果出现错误,则使用fmt.Println将错误打印到标准输出。 如果没有错误,我们打印Capitalized name: SAMMY

尝试将名称中的字符串"sammy" name, err := capitalize("sammy")更改为空字符串("") ,您将收到错误Could not capitalize: no name provided

当函数的调用者为name参数提供空字符串时, capitalize函数将返回错误。 name参数不是空字符串时, capitalize()使用strings.ToTitle来大写name参数,并为错误值返回nil

这个示例遵循一些微妙的约定,这是Go代码的典型,但Go编译器没有强制执行。 当函数返回多个值(包括错误)时,约定会请求我们将error作为最后一项返回。 当从具有多个返回值的函数返回error ,惯用的Go代码也会将每个非错误值设置为零值 例如,零值是字符串的空字符串,整数的0 ,结构类型的空结构,接口和指针类型的nil ,仅举几例。 我们在变量和常量教程中更详细地介绍了零值。

减少样板

在有许多值从函数返回的情况下,遵守这些约定会变得乏味。 我们可以使用匿名函数来帮助减少样板。 匿名函数是分配给变量的过程。 与我们在前面的示例中定义的函数相比,它们仅在您声明它们的函数中可用 - 这使得它们完美地充当可重用辅助逻辑的简短片段。

以下程序修改最后一个示例以包括我们正在大写的名称的长度。 由于它有三个值要返回,如果没有匿名函数来帮助我们,处理错误可能变得很麻烦:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, int, error) {
    handle := func(err error) (string, int, error) {
        return "", 0, err
    }

    if name == "" {
        return handle(errors.New("no name provided"))
    }

    return strings.ToTitle(name), len(name), nil
}

func main() {
    name, size, err := capitalize("sammy")
    if err != nil {
        fmt.Println("An error occurred:", err)
    }

    fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
Capitalized name: SAMMY, length: 5

main() ,我们现在分别从capitalize捕获三个返回的参数作为namesizeerr 然后我们通过检查err变量是否不等于nil来检查以查看capitalize返回error 在尝试使用capitalize返回的任何其他值之前,这很重要,因为匿名函数handle可以将这些值设置为零值。 由于我们没有提供错误,因为我们提供了字符串"sammy" ,我们打印出大写的名称及其长度。

再次,您可以尝试将"sammy"更改为空字符串("")以查看打印的错误案例( An error occurred: no name provided )。

capitalize ,我们将handle变量定义为匿名函数。 它只需要一个错误,并以与capitalize的返回值相同的顺序返回相同的值。 handle将这些值设置为零值,并将作为参数传递的error转发为最终返回值。 使用这个,我们可以通过使用调用前面的return语句return capitalize遇到的任何错误,并以error作为参数进行handle

请记住, capitalize必须始终返回三个值,因为这就是我们定义函数的方式。 有时我们不想处理函数可以返回的所有值。 幸运的是,我们可以灵活地在分配方面使用这些值。

处理多返回函数的错误

当函数返回许多值时,Go要求我们将每个值分配给变量。 在最后一个示例中,我们通过为从capitalize函数返回的两个值提供名称来实现此目的。 这些名称应以逗号分隔,并显示在:=运算符的左侧。 capitalize返回的第一个值将分配给name变量,第二个值( error )将分配给变量err 偶尔,我们只对错误值感兴趣。 您可以使用特殊_变量名称丢弃函数返回的任何不需要的值。

在下面的程序中,我们修改了第一个涉及capitalize函数的例子,通过传入空字符串("")来产生错误。 尝试运行此程序,看看我们如何通过使用_变量丢弃第一个返回值来检查错误:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}
Could not capitalize: no name provided

在这次main()函数中,我们将大写的名称(首先返回的string )分配给下划线变量( _ )。 同时,我们将由capitalize返回的error分配给err变量。 然后我们检查if if err != nil条件中if err != nil存在if err != nil 由于我们将空字符串硬编码为参数以capitalize_, err := capitalize("") ,因此该条件将始终求值为true 这会产生输出"Could not capitalize: no name provided"通过调用if语句正文中的fmt.Println函数来打印。 此后的return将跳过fmt.Println("Success!")

结论

我们已经看到很多使用标准库创建错误的方法,以及如何构建以惯用方式返回错误的函数。 在本教程中,我们设法使用标准库errors.Newfmt.Errorf函数成功创建了各种错误。 在以后的教程中,我们将介绍如何创建自己的自定义错误类型,以便向用户传达更丰富的信息。