健壮的代码需要对用户输入错误、网络连接故障和磁盘故障等意外情况做出正确反应。 错误处理是识别程序何时处于意外状态并采取步骤记录诊断信息以供以后调试的过程。
与其他需要开发人员使用专门的语法处理错误的语言不同,Go 中的错误是从函数返回的类型为 error
的值,就像任何其他值一样。 为了处理 Go 中的错误,我们必须检查函数可能返回的这些错误,确定是否发生了错误,并采取适当的措施来保护数据并告诉用户或操作员发生了错误。
创建错误
在我们处理错误之前,我们需要先创建一些。 标准库提供了两个内置函数来创建错误:errors.New
和 fmt.Errorf
。 这两个函数都允许您指定稍后可以呈现给用户的自定义错误消息。
errors.New
采用单个参数——作为字符串的错误消息,您可以自定义该字符串以提醒您的用户出了什么问题。
尝试运行以下示例以查看由 errors.New
创建的错误打印到标准输出:
package main import ( "errors" "fmt" ) func main() { err := errors.New("barnacles") fmt.Println("Sammy says:", err) }
OutputSammy says: barnacles
我们使用标准库中的 errors.New
函数来创建一个新的错误消息,其中字符串 "barnacles"
作为错误消息。 正如 Go 编程语言样式指南 所建议的,我们在这里遵循约定,将错误消息使用小写。
最后,我们使用 fmt.Println
函数将我们的错误消息与 "Sammy says:"
结合起来。
fmt.Errorf
函数允许您动态构建错误消息。 它的第一个参数是一个字符串,其中包含带有占位符值的错误消息,例如字符串的 %s
和整数的 %d
。 fmt.Errorf
将此格式化字符串后面的参数按顺序插入到这些占位符中:
package main import ( "fmt" "time" ) func main() { err := fmt.Errorf("error occurred at: %v", time.Now()) fmt.Println("An error happened:", err) }
OutputAn 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
函数将结果打印到标准输出。
处理错误
通常,您不会看到这样创建的错误会立即用于其他目的,就像前面的示例一样。 在实践中,更常见的是创建错误并在出现问题时从函数中返回它。 然后该函数的调用者将使用 if
语句来查看是否存在错误或 nil
- 一个未初始化的值。
下一个示例包含一个始终返回错误的函数。 请注意,当您运行程序时,它会产生与上一个示例相同的输出,即使这次函数返回了错误。 在其他位置声明错误不会更改错误消息。
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!") }
OutputAn 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 语句有一个可选的赋值子句,可用于帮助压缩函数调用并处理其错误。
运行下一个程序以查看与前面示例相同的输出,但这次使用复合 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!") }
OutputAn error occurred: barnacles
和以前一样,我们有一个函数,boom()
,它总是返回一个错误。 我们将 boom()
返回的错误分配给 err
作为 if
语句的第一部分。 在 if
语句的第二部分,在分号之后,err
变量可用。 我们检查是否存在错误,并像之前所做的那样使用短前缀字符串打印我们的错误。
在本节中,我们学习了如何处理只返回错误的函数。 这些函数很常见,但能够处理来自可以返回多个值的函数的错误也很重要。
与值一起返回错误
返回单个错误值的函数通常是那些影响某些状态更改的函数,例如向数据库插入行。 编写函数也很常见,如果它们成功完成则返回一个值,如果该函数失败则返回一个潜在的错误。 Go 允许函数返回多个结果,这些结果可用于同时返回一个值和一个错误类型。
为了创建一个返回多个值的函数,我们在函数签名的括号内列出每个返回值的类型。 例如,返回 string
和 error
的 capitalize
函数将使用 func capitalize(name string) (string, error) {}
声明。 (string, error)
部分告诉 Go 编译器该函数将按顺序返回 string
和 error
。
运行以下程序以查看返回 string
和 error
的函数的输出:
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) }
OutputCapitalized name: SAMMY
我们将 capitalize()
定义为一个函数,它接受一个字符串(要大写的名称)并返回一个字符串和一个错误值。 在 main()
中,我们调用 capitalize()
并将函数返回的两个值分配给 name
和 err
变量,并在左侧用逗号分隔它们 - :=
运算符的手边。 之后,我们像前面的示例一样执行 if err != nil
检查,如果存在错误,则使用 fmt.Println
将错误打印到标准输出。 如果不存在错误,我们打印 Capitalized name: SAMMY
。
尝试将 name, err := capitalize("sammy")
中的字符串 "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) }
OutputCapitalized name: SAMMY, length: 5
在 main()
中,我们现在将 capitalize
的三个返回参数分别捕获为 name
、size
和 err
。 然后我们通过检查 err
变量是否不等于 nil
来检查 capitalize
是否返回了 error
。 在尝试使用 capitalize
返回的任何其他值之前,这很重要,因为匿名函数 handle
可以将这些值设置为零。 由于我们提供了字符串 "sammy"
没有发生错误,因此我们打印出大写名称及其长度。
再次,您可以尝试将 "sammy"
更改为空字符串 ("")
以查看打印的错误情况 (An error occurred: no name provided
)。
在 capitalize
中,我们将 handle
变量定义为匿名函数。 它接受一个错误并以与 capitalize
的返回值相同的顺序返回相同的值。 handle
将这些值设置为零值,并将作为其参数传递的 error
作为最终返回值转发。 使用它,我们可以通过在调用 handle
之前使用 return
语句返回在 capitalize
中遇到的任何错误,并将 error
作为其参数.
请记住,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!") }
OutputCould not capitalize: no name provided
这次在 main()
函数中,我们将大写名称(首先返回的 string
)分配给下划线变量(_
)。 同时,我们将capitalize
返回的error
赋值给err
变量。 然后我们检查 if err != nil
条件中是否存在错误。 由于我们在 _, err := capitalize("")
行中硬编码了一个空字符串作为 capitalize
的参数,因此该条件将始终计算为 true
。 这将生成通过调用 if
语句主体内的 fmt.Println
函数打印的输出 "Could not capitalize: no name provided"
。 之后的 return
将跳过 fmt.Println("Success!")
。
结论
我们已经看到了许多使用标准库创建错误的方法,以及如何构建以惯用方式返回错误的函数。 在本教程中,我们成功地使用标准库 errors.New
和 fmt.Errorf
函数创建了各种错误。 在以后的教程中,我们将了解如何创建我们自己的自定义错误类型以向用户传达更丰富的信息。