error 是啥?

错误表示程序中的异常状态。假设我们试图打开一个文件,但该文件在文件系统中不存在。这就是一种异常情况,用错误来表示。

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

就像任何其他内置类型,如 int,float64,… 错误值可以存储在变量中,从函数返回等等。


Example

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


  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. f, err := os.Open("/test.txt")
  8. if err != nil {
  9. fmt.Println(err)
  10. return
  11. }
  12. fmt.Println(f.Name(), "opened successfully")
  13. }

Run in playground

上面的程序第 9 行中,我们试图在路径 /test.txt(在 playground 上显然不存在)中打开文件。os 包的 Open 函数具有以下定义,

func Open(name string) (file *File, err error)

如果文件已成功打开,则 Open 函数将返回文件处理程序,错误将为 nil。如果在打开文件时出错,将返回非零错误。

如果函数或方法返回错误,那么按照惯例,它必须是函数返回的最后一个值。因此,Open 函数返回 err 作为最后一个值。

在 Go 中处理错误的惯用方法是将返回的错误与 nil 进行比较。 nil 值表示没有发生错误,非 nil 值表示存在错误。在上面例子中,我们在第 10 行中检查错误是否没有。如果它不是 nil,我们只需打印错误并从 main 函数返回。

运行程序输出


  1. open /test.txt: No such file or directory

完美😃。我们收到一条错误消息,指出该文件不存在。

错误类型表示

让我们深入一点,看看如何定义内置 error 类型。 error 具有以下定义的接口类型。


  1. type error interface {
  2. Error() string
  3. }

它包含一个带有签名 Error() string 的方法。实现此接口的任何类型都可以用作错误。此方法提供错误的描述。

打印错误时,fmt.Println 函数在内部调用 Error() string 方法以获取错误的描述。这就是上述示例程序第 11 行的错误描述的打印方式。

从错误中提取更多信息的不同方法

现在我们知道 error 是一种接口类型,让我们看看如何提取有关错误的更多信息。

在上面我们看到的例子中,我们刚刚打印了错误的描述。如果我们想要导致错误的文件的实际路径,该怎么办?一种可能的方法是解析错误字符串。这是我们程序的输出,


  1. open /test.txt: No such file or directory

我们可以解析此错误消息并获取导致错误的文件的文件路径“/test.txt”,但这是一种糟糕的方式。错误描述可以随时在较新版本的语言中更改,我们的代码将会中断。

有没有办法可靠地获取文件名?答案是肯定的,Go 的标准库使用不同的方式来提供有关错误的更多信息。让我们一个一个来了解它们。

1.断言结构类型底层并从结构字段中获取更多信息

如果仔细阅读 Open 函数的文档,可以看到它返回类型为 *PathError 的错误。PathError 是一种结构类型,它在标准库中的实现如下,


  1. type PathError struct {
  2. Op string
  3. Path string
  4. Err error
  5. }
  6. func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

如果你有兴趣知道上述源代码的完整实现,可以在此处找到 https://golang.org/src/os/error.go?s=653:716#L11

从上面的代码中,你可以理解 *PathError 通过声明 Error() string 方法实现了 error interface。该方法将操作、路径和实际错误连接起来并返回。因此,我们得到了错误消息,


  1. open /test.txt: No such file or directory

PathError 结构的 Path 字段包含导致错误的文件的路径。让我们修改上面编写的程序并打印路径。


  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. f, err := os.Open("/test.txt")
  8. if err, ok := err.(*os.PathError); ok {
  9. fmt.Println("File at path", err.Path, "failed to open")
  10. return
  11. }
  12. fmt.Println(f.Name(), "opened successfully")
  13. }

Run in playground

在上面的程序中,我们在第 10 行使用 type assertion 来获取错误接口的底层值。 然后我们使用 err.Path 打印路径。 该程序输出,


  1. File at path /test.txt failed to open

太好了😃。我们已经成功地使用类型断言从错误中获取文件路径。

2.断言底层结构类型并使用方法获取更多信息

获取更多信息的第二种方法是断言底层类型,并通过调用 struct 类型的方法获取更多信息

让我们通过一个例子更好地理解这一点。

标准库中的 DNSError 结构类型定义如下,


  1. type DNSError struct {
  2. ...
  3. }
  4. func (e *DNSError) Error() string {
  5. ...
  6. }
  7. func (e *DNSError) Timeout() bool {
  8. ...
  9. }
  10. func (e *DNSError) Temporary() bool {
  11. ...
  12. }

从上面的代码中可以看出,DNSError 结构有两个方法 Timeout() boolTemporary() bool,它们返回一个布尔值,指示错误是由于超时还是暂时的。

让我们编写一个断言 *DNSError 类型的程序,并调用这些方法来确定错误是暂时的还是由于超时。


  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. )
  6. func main() {
  7. addr, err := net.LookupHost("golangbot123.com")
  8. if err, ok := err.(*net.DNSError); ok {
  9. if err.Timeout() {
  10. fmt.Println("operation timed out")
  11. } else if err.Temporary() {
  12. fmt.Println("temporary error")
  13. } else {
  14. fmt.Println("generic error: ", err)
  15. }
  16. return
  17. }
  18. fmt.Println(addr)
  19. }

注意:DNS 查找在 playground 不起作用。请在本地计算机上运行此程序。

在上面的程序中,我们试图在第 9 行中获取无效域名 golangbot123.com 的 IP 地址。 在第 10 行我们通过断言 *net.DNSError 来获取错误的基础值。然后我们在 11 和 13 行检查错误是由于超时还是临时错误。

在我们的例子中,错误既不是临时错误也不是超时错误,因此程序将输出


  1. generic error: lookup golangbot123.com: no such host

如果错误是临时或者超时错误,则相应的 if 语句将被执行,我们可以妥善处理。


3.直接比较

获得关于错误的更多信息的第三种方法是直接与类型为 error 的变量进行比较。让我们通过一个例子来理解它。

filepath 包的 Glob 函数用于返回与模式匹配的所有文件的名称。当模式异常时,此函数返回一个错误 ErrBadPattern

ErrBadPatternfilepath 包中定义如下。


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

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

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


  1. package main
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. )
  6. func main() {
  7. files, error := filepath.Glob("[")
  8. if error != nil && error == filepath.ErrBadPattern {
  9. fmt.Println(error)
  10. return
  11. }
  12. fmt.Println("matched files", files)
  13. }

Run in playground

在上面的程序中,我们搜索模式文件 [,这是一个格式错误的模式。我们检查错误是否为 nil。要获得有关错误的更多信息,我们直接将它与 filepath.ErrBadPattern 进行比较。如果条件满足,则错误是由于格式错误。该程序将输出,


  1. syntax error in pattern

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

不要忽略错误

永远不要忽视错误。忽视错误是自找麻烦。让我重写这个例子,它列出了所有匹配模式的文件的名称,忽略了错误处理代码。


  1. package main
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. )
  6. func main() {
  7. files, _ := filepath.Glob("[")
  8. fmt.Println("matched files", files)
  9. }

Run in playground

我们从前面的例子中已经知道模式是无效的。 我在第 9 行通过使用 _ 标识符忽略了Glob 函数返回的错误。 我在第 10 行只打印匹配的文件。 该程序将输出,


  1. matched files []

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

这就是本教程的结尾。

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

  • 什么是错误


  • 错误的表示


  • 从错误中提取更多信息的各种方法


  • 不要忽视错误

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

原文链接

https://golangbot.com/error-handling/