错误处理
Q7nl1s admin

什么是错误?

错误表示程序中发生的任何异常情况。比方说,我们试图打开一个文件,但该文件在文件系统中并不存在。这是一个异常的情况,它被表示为一个错误。

Go中的错误是普通的值。就像任何其他内置类型,如int, float64, …错误值可以存储在变量中,作为参数传递给函数,从函数中返回,等等。

错误用内置的error类型来表示。我们将在本教程的后面学习更多关于error类型的知识。

例子

让我们马上从一个试图打开一个不存在的文件的例子程序开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)

func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully")
}

Run in playground

在上述程序的第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
2
3
type error interface {  
Error() string
}

它包含一个签名为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
2
3
4
5
6
7
type PathError struct {  
Op string
Path string
Err error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

如果你有兴趣知道上述源代码存在的地方,可以在这里找到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的简单描述是,它试图将错误转换为错误类型,并返回truefalse,表明转换成功与否。

一个程序将使事情变得清晰。让我们修改上面写的程序,用As函数打印路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"errors"
"fmt"
"os"
)

func main() {
f, err := os.Open("test.txt")
if err != nil {
var pErr *os.PathError
if errors.As(err, &pErr) {
fmt.Println("Failed to open file at path", pErr.Path)
return
}
fmt.Println("Generic error", err)
return
}
fmt.Println(f.Name(), "opened successfully")
}

Run in playground

在上述程序中,我们首先在第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
2
3
4
5
6
7
8
9
10
11
12
13
type DNSError struct {  
...
}

func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}

DNSError结构有两个方法Timeout() boolTemporary() bool,它们返回一个布尔值,表明error是由于超时还是临时的。

让我们写一个程序,将错误转换为*DNSError类型,并调用上述方法来确定错误是临时的还是由于超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"errors"
"fmt"
"net"
)

func main() {
addr, err := net.LookupHost("golangbot123.com")
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.Timeout() {
fmt.Println("operation timed out")
return
}
if dnsErr.Temporary() {
fmt.Println("temporary error")
return
}
fmt.Println("Generic DNS error", err)
return
}
fmt.Println("Generic error", err)
return
}
fmt.Println(addr)
}

注意: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

ErrBadPatternfilepath包中被定义为一个全局变量。

1
var ErrBadPattern = errors.New("syntax error in pattern")  

errors.New()用于创建一个新的error。我们将在下一个教程中详细讨论这个问题。

ErrBadPattern在模式畸形时由Glob函数返回。

让我们写一个小程序来检查这个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"errors"
"fmt"
"path/filepath"
)

func main() {
files, err := filepath.Glob("[")
if err != nil {
if errors.Is(err, filepath.ErrBadPattern) {
fmt.Println("Bad pattern error:", err)
return
}
fmt.Println("Generic error:", err)
return
}
fmt.Println("matched files", files)
}

Run in playground

在上面的程序中,我们搜索模式 [ 的文件,这是一个畸形的模式。我们检查error是否为nil。为了获得更多关于error的信息,我们直接将其与filepath.ErrBadPattern在第12行进行比较。与As类似,Is函数在一个错误链上工作。我们将在下一个教程中学习更多这方面的知识。
在本教程中,Is函数可以被认为是在传递给它的两个错误都相同时返回true

Is函数在第12行中返回true,因为error是由畸形的模式引起的,这个程序将打印:

1
Bad pattern error: syntax error in pattern  

标准库使用上述任何一种方式来提供关于错误的更多信息。我们将在下一个教程中使用这些方式来创建我们自己的自定义错误。

不要忽视错误

永远不要忽视一个错误。忽视错误是在招惹麻烦。让我重写这个例子,它列出了所有符合模式的文件的名称,忽略了错误。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"path/filepath"
)

func main() {
files, _ := filepath.Glob("[")
fmt.Println("matched files", files)
}

Run in playground

从前面的例子中我们已经知道,这个模式是无效的。我通过在第9行使用_空白标识符,忽略了Glob函数返回的error。我在第9行简单地打印了匹配的文件。这个程序会打印出来。

1
matched files []  

由于我们忽略了这个error,输出结果看起来好像没有文件与模式相匹配,但实际上模式本身是错误的。所以永远不要忽视错误。

这样我们就到了本教程的结尾。

在本教程中,我们讨论了如何处理程序中出现的错误,以及如何检查错误以从中获得更多信息。简单回顾一下我们在本教程中讨论的内容。

  • 什么是错误?
  • 错误表述
  • 从错误中提取更多信息的各种方法
  • 不要忽视错误

在下一个教程中,我们将创建我们自己的自定义错误,同时为我们的自定义错误添加更多的上下文。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View