Go中的错误处理

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

不像其他语言要求开发人员使用特殊的语法来处理错误,Go中的错误是从函数中返回类型为error的值,就像任何其他值一样。要在Go中处理错误,我们必须检查函数可能返回的这些错误,确定是否发生了错误,并采取适当的行动保护数据,并告诉用户或操作人员错误发生了。

创建错误

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

errors.New接受一个参数------一个字符串形式的错误消息,你可以自定义它来警告你的用户发生了什么错误。

试着运行下面的例子,看看errors.New创建的错误会打印到标准输出:

go 复制代码
package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.New("barnacles")
	fmt.Println("Sammy says:", err)
}
shell 复制代码
OutputSammy says: barnacles

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

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

fmt.Errorf函数允许你动态构建错误消息。它的第一个参数是一个包含错误消息的字符串,其中包含占位符值,例如字符串为%s,整数为%dfmt.Errorf将格式化字符串后面的参数按顺序插入到这些占位符中:

go 复制代码
package main

import (
	"fmt"
	"time"
)

func main() {
	err := fmt.Errorf("error occurred at: %v", time.Now())
	fmt.Println("An error happened:", err)
}
shell 复制代码
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------一个未初始化的值。

下面的例子包含了一个总是返回错误的函数。请注意,当你运行这个程序时,尽管函数这次返回了错误,但它的输出与前一个例子相同。在不同的位置声明错误并不会改变错误消息。

go 复制代码
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!")
}
shell 复制代码
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代码自然在第一个缩进级别拥有它的"happy path"逻辑,在第二个缩进级别拥有所有的"sad path"逻辑。

If语句有一个可选的赋值子句,可用于简化函数调用和错误处理。

运行下一个程序,会看到与之前示例相同的输出,但这次使用了复合if语句来减少一些样板代码:

go 复制代码
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!")
}
shell 复制代码
OutputAn error occurred: barnacles

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

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

在值旁边返回错误

返回单个错误值的函数通常会影响一些有状态的更改,比如向数据库中插入行。编写函数时,如果成功返回一个值,如果失败则返回一个潜在的错误,这种情况也很常见。Go允许函数返回多个结果,可用于同时返回值和错误类型。

要创建返回多个值的函数,可以在函数签名的括号中列出每个返回值的类型。例如,一个返回stringerrorcapitalize函数可以使用func capitalize(name string) (string, error){}来声明。(string, error)部分告诉Go编译器,这个函数将返回一个string和一个error,按照这个顺序。

运行下面的程序,看看这个同时返回stringerror的函数的输出:

go 复制代码
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)
}
shell 复制代码
OutputCapitalized name: SAMMY

我们将capitalize()定义为一个函数,它接受一个字符串(要大写的名字)作为参数,并返回一个字符串和一个错误值。在main()中,我们调用capitalize(),并将函数返回的两个值赋值给nameerr变量,方法是在:=操作符的左边用逗号分隔它们。在这之后,我们执行if err != nil检查,就像前面的例子一样,如果错误存在,使用fmt.Println将错误打印到标准输出。如果没有报错,我们打印Capitalized name: SAMMY

试着将name, err := capitalize("sammy")中的字符串"sammy"改为空字符串(""),你将得到错误信息Could not capitalize: no name provided

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

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

减少样板

在函数需要返回很多值的情况下,遵守这些约定会变得很乏味。我们可以使用匿名函数来帮助减少样板代码。匿名函数是被赋值给变量的过程。与前面示例中定义的函数不同,它们只在声明它们的函数中可用,这使它们成为可重用的辅助逻辑片段。

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

go 复制代码
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)
}
shell 复制代码
OutputCapitalized name: SAMMY, length: 5

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

同样,你可以尝试将"sammy"改为空字符串(""),以查看打印的错误情况(An error occurred: no name provided)。

capitalize中,我们将handle变量定义为一个匿名函数。它接受一个错误,并以与capitalize相同的顺序返回相同的值。handle将这些值设置为零,并将作为参数传递的error作为最终返回值。使用它,我们可以通过在调用handle之前使用return语句将error作为其参数来返回在capitalize中遇到的任何错误。

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

处理来自多重返回函数的错误

当函数返回许多值时,Go要求我们将每个值分配给一个变量。在上一个例子中,我们通过为capitalize函数返回的两个值提供名称来实现这一点。这些名称应该用逗号分隔,并出现在:=操作符的左侧。从capitalize返回的第一个值将被赋值给name变量,第二个值(error)将被赋值给变量err。有时候,我们只对误差值感兴趣。你可以使用特殊的_变量名丢弃函数返回的任何不需要的值。

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

go 复制代码
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!")
}
shell 复制代码
OutputCould not capitalize: no name provided

这次在main()函数中,我们将首字母大写的名字(首先返回的string)赋值给下划线变量(_)。同时,我们将capitalize返回的error赋值给err变量。然后我们在条件语句if err != nil中检查错误是否存在。因为我们硬编码了一个空字符串作为capitalize_, err:= capitalize("")这一行的参数,所以这个条件语句总是被求值为true。这产生了在if语句体中调用fmt.Println函数输出的"Could not capitalize: no name provided"。之后的return将跳过fmt.Println("Success!")

总结

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

相关推荐
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
向前看-3 小时前
验证码机制
前端·后端
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar4 小时前
速通Python 第三节
开发语言·python
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
信号处理学渣5 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客5 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s5 小时前
Pandas
开发语言·python