什么是错误?
错误表示程序中发生的任何异常情况。比方说,我们试图打开一个文件,但该文件在文件系统中并不存在。这是一个异常的情况,它被表示为一个错误。
Go中的错误是普通的值。就像任何其他内置类型,如int, float64, …错误值可以存储在变量中,作为参数传递给函数,从函数中返回,等等。
错误用内置的error
类型来表示。我们将在本教程的后面学习更多关于error
类型的知识。
例子
让我们马上从一个试图打开一个不存在的文件的例子程序开始。
在上述程序的第9行中,我们试图打开路径为/test.txt
的文件(该文件显然不存在于playground中)。os
包的Open函数有如下签名。
func Open(name string) (*File, error)
如果文件已经被成功打开,那么 Open 函数将返回文件处理程序,错误将为nil
。如果在打开文件时出现了错误,那么将返回一个非nil
的错误。
如果一个函数或方法返回一个error,那么按照惯例,它必须是该函数返回的最后一个值。因此,Open函数将error
作为最后一个值返回。
Go中处理错误的惯用方法是将返回的错误与nil
进行比较。一个nil
值表示没有错误发生,一个非nil
值表示有错误存在。在我们的例子中,我们在第10行检查错误是否为nil
。如果不是nil
,我们就简单地打印错误并从main
函数返回。
运行这个程序将打印
1 | open /test.txt: No such file or directory |
Perfect 😃。我们得到一个错误,说明该文件不存在。
错误类型表示
让我们再深入挖掘一下,看看内置的error
类型是如何定义的。error是一个接口类型,其定义如下。
1 | type error interface { |
它包含一个签名为Error() string
的方法。任何实现这个接口的类型都可以作为一个错误。这个方法提供了错误的描述。
当打印错误时,fmt.Println
函数在内部调用Error() string
方法,以获得error的描述。这就是error描述在第11行的打印方式。
从错误中提取更多信息的不同方法
现在我们知道error是一种接口类型,让我们看看如何提取关于错误的更多信息。
在我们上面看到的例子中,我们只是打印了error的描述。如果我们想知道导致error的文件的实际路径,该怎么办呢?一种可能的方法是解析error字符串。以下就是我们程序的输出。
1 | open /test.txt: No such file or directory |
我们可以解析这个错误信息,得到导致错误的文件的路径”/test.txt”,但这是一个脏方法。在较新版本的Go中,错误描述随时可能改变,我们的代码就会被破坏。
有没有更好的方法来获取文件名🤔?答案是肯定的,Go标准库使用不同的方式来提供更多的错误信息。让我们逐一来看一下。
1. 将错误转换为底层类型并从结构字段中检索更多信息
如果你仔细阅读Open函数的文档,你可以看到它返回一个*PathError
类型的错误。PathError
是一个结构类型,它在标准库中的实现如下。
1 | type PathError struct { |
如果你有兴趣知道上述源代码存在的地方,可以在这里找到https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/io/fs/fs.go;l=250
从上面的代码中,你可以了解到,*PathError
通过声明Error()
字符串方法实现了error interface
。这个方法将操作、路径和实际的错误连接起来并返回。因此我们得到了错误信息。
1 | open /test.txt: No such file or directory |
PathError
结构的Path
字段包含了导致错误的文件的路径。
我们可以使用erros
包中的As函数来将error
转换为它的基本类型。As
函数的描述中提到了error chain(错误链)。现在请忽略它。当我们在下一个教程中学习自定义错误时,我们将了解错误链和包装是如何工作的。
对As
的简单描述是,它试图将错误转换为错误类型,并返回true
或false
,表明转换成功与否。
一个程序将使事情变得清晰。让我们修改上面写的程序,用As
函数打印路径。
在上述程序中,我们首先在第11行检查error是否为nil
。然后我们在第11行使用As
函数并在13行将err
转换为*os.PathError
。如果转换成功,As
将返回true
。然后我们在第14行使用pErr.Path
打印路径。
如果你想知道为什么pErr
是一个指针,原因是:error interface
是由PathError
的指针实现的,因此pErr
是一个指针。下面的代码显示,*PathError
实现了error interface
。
1 | func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } |
As
函数要求第二个参数是一个实现eror的类型的指针。因此,我们传递&perr
。
这个程序输出。
1 | Failed to open file at path test.txt |
如果底层错误不是*os.PathError
类型的,控件将到达第17行。并打印出一个通用的错误信息。
很好😃。我们已经成功地使用As
函数从error中获得文件路径。
2. 使用方法检索更多信息
从error中获取更多信息的第二个方法是找出底层类型,并通过调用结构类型上的方法获取更多信息。
让我们通过一个例子来更好地理解这一点。
标准库中的DNSError结构类型定义如下。
1 | type DNSError struct { |
DNSError
结构有两个方法Timeout() bool
和Temporary() bool
,它们返回一个布尔值,表明error是由于超时还是临时的。
让我们写一个程序,将错误转换为*DNSError
类型,并调用上述方法来确定错误是临时的还是由于超时。
注意:DNS查询在playground中不工作。请在你的本地机器上运行这个程序。
在上面的程序中,在第9行,我们试图获得一个无效域名golangbot123.com
的IP地址。在第13行,我们通过使用As
函数并将其转换为*net.DNSError
来获得error的基本值。然后我们在第14行和第18行分别检查该错误是由于超时还是暂时的。
在我们的案例中,error既不是临时的,也不是由于超时,因此程序将打印。
1 | Generic DNS error lookup golangbot123.com: no such host |
如果error是临时的或由于超时,那么相应的if语句就会执行,我们可以适当地处理它。
3. 直接比较
获得一个错误的更多细节的第三个方法是与一个error
类型的变量直接比较。让我们通过一个例子来理解这一点。
filepath
包的Glob函数被用来返回与一个模式匹配的所有文件的名称。当模式是畸形(malformed)的时候,这个函数返回错误ErrBadPattern
。
ErrBadPattern在filepath
包中被定义为一个全局变量。
1 | var ErrBadPattern = errors.New("syntax error in pattern") |
errors.New()
用于创建一个新的error。我们将在下一个教程中详细讨论这个问题。
ErrBadPattern
在模式畸形时由Glob
函数返回。
让我们写一个小程序来检查这个错误。
在上面的程序中,我们搜索模式 [
的文件,这是一个畸形的模式。我们检查error
是否为nil
。为了获得更多关于error
的信息,我们直接将其与filepath.ErrBadPattern
在第12行进行比较。与As
类似,Is
函数在一个错误链上工作。我们将在下一个教程中学习更多这方面的知识。
在本教程中,Is
函数可以被认为是在传递给它的两个错误都相同时返回true
。
Is
函数在第12行中返回true
,因为error
是由畸形的模式引起的,这个程序将打印:
1 | Bad pattern error: syntax error in pattern |
标准库使用上述任何一种方式来提供关于错误的更多信息。我们将在下一个教程中使用这些方式来创建我们自己的自定义错误。
不要忽视错误
永远不要忽视一个错误。忽视错误是在招惹麻烦。让我重写这个例子,它列出了所有符合模式的文件的名称,忽略了错误。
1 | package main |
从前面的例子中我们已经知道,这个模式是无效的。我通过在第9行使用_
空白标识符,忽略了Glob
函数返回的error
。我在第9行简单地打印了匹配的文件。这个程序会打印出来。
1 | matched files [] |
由于我们忽略了这个error
,输出结果看起来好像没有文件与模式相匹配,但实际上模式本身是错误的。所以永远不要忽视错误。
这样我们就到了本教程的结尾。
在本教程中,我们讨论了如何处理程序中出现的错误,以及如何检查错误以从中获得更多信息。简单回顾一下我们在本教程中讨论的内容。
- 什么是错误?
- 错误表述
- 从错误中提取更多信息的各种方法
- 不要忽视错误
在下一个教程中,我们将创建我们自己的自定义错误,同时为我们的自定义错误添加更多的上下文。
v1.5.2