错误处理

Golang中的错误类型

程序执行的过程中会发生的异常情况称为错误。比如打开一个文件系统不存在的文件就会发生错误。

Golang中有个专门用来表示错误的接口类型:error

intfloat64等其他数据类型类似,error类型的值也可以拿来赋值给变量,传参给函数,或者作为函数返回值。

举例

我们举个例子,尝试打开一个系统不存在的文件。

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")
}

这里我们尝试打开路径为/test.txt的文件。os.Open函数的签名如下:

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

如果文件打开成功,则Open函数返回值file为该文件的指针,同时返回值errnil。如果读取文件发生异常,比如文件不存在,则返回值err不为nil

在Golang中,如果一个函数/方法会返回错误,一般都是作为最后一个返回值,这里的Open函数返回的错误就是位于第二个位置,也就是最后一个位置。

通常情况下我们都是将函数返回的error字段跟nil比较。如果是nil,则表示无错误,否则表示有错误发生。

前面的程序我们就是将errornil比较,来判断是否正常打开文件。该程序执行结果为:

open /test.txt: No such file or directory

很明显,该错误信息表示文件/test.txt不存在(我的机器确实不存在该文件,你的机器应该也不会凑巧存在该文件吧)。

error接口类型

Golang中error是一个接口类型,定义如下:

type error interface {
    Error() string
}

该接口仅包含一个方法Error() string,该方法用来表示错误信息。任何实现了该接口的类型都是错误类型。当我们通过fmt.Println打印一个错误类型的变量时,其实就是调用其Error() string方法获取错误信息并打印。

获取更多错误信息

Golang中的error是一个接口类型,现在我们看下如果从error变量中提取更多错误相关的信息。

前面的例子中我们打印出了错误的描述信息,但是如果我们想获取发生错误的文件路径怎么实现呢?

这肯定是能够实现的,比如我们打印出的错误信息里面就有这个信息,可以解析出来。但是直接从错误文本信息中解析文件路径非常不明智,我们应该寻找更加正规靠谱的方法。

1.断言error具体类型并通过其属性获取信息

我们看下Open函数的官方文档会发现,该函数返回的错误字段的类型为*PathErrorPathError是一个结构体类型,该类型实现了Error() string方法,实现了error接口,因此属于error类型。

type PathError struct {  
    Op   string
    Path string
    Err  error
}

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

这几行源码可以参考https://golang.org/src/os/error.go?s=653:716#L11

Error()函数的实现我们就能理解前面的输出了:

open /test.txt: No such file or directory

结构体类型PathError的属性Path就是用来表示文件路径的,因此我们可以读取该字段来获取文件路径。

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

这里我们通过类型断言来获取了error接口内部的具体值。接着我们通过err.Path来获取文件路径。该程序输出为:

File at path /test.txt failed to open

2.断言error具体类型并通过其方法获取信息

当我们断言出error内部的具体类型后,不仅可以通过其属性获取更多有用信息,还可以调用其方法来获取信息。我们通过例子来理解下。在Golang标准库中,结构体类型DNSError的定义如下:

type DNSError struct {  
    ...
}

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

该结构体类型有两个方法Timeout() boolTemporary() bool用来表示具体错误原因是超时还是暂时解析失败。下面我们实现一个例子,来断言*DNSError类型,如果断言成功则调用其方法来判断错误原因。

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}

这里我们尝试获取域名golangbot123.com的ip地址,由于该域名是不存在的,因此会报域名解析错误,即net.DNSError类型的错误。我们断言该类型错误,然后调用该类型错误上的方法来判断具体错误原因。该程序运行结果如下:

generic error:  lookup golangbot123.com: no such host

3.直接判断相等

第三种获取更多错误相关信息的方法就是直接将错误字段跟某个error类型的已有变量做比较。我们还是通过例子来了解下。

filepath包中有个函数Glob用来返回跟某个正则匹配的文件目录。如果正则写的格式有问题会返回一个错误变量ErrBadPatternErrBadPatternfilepath包中定义为:

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

这里errors.New()用来创建一个error类型的变量。由于这里定义的ErrBadPattern是大写开头的,因此属于导出型变量,我们能够直接在包外访问到该变量。下面我们通过例子来判断下错误是否是ErrBadPattern

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}

这里我们传给Glob的参数是[,格式是错误的(具体为什么是错误的可以去看Glob相关文档)。然后我们直接将返回的错误字段跟变量filepath.ErrBadPattern去比较是否相等,如果相等则证明确实是该错误。该程序执行结果如下:

syntax error in pattern

Golang中基本都是基于这3种方式来提供更多错误相关的细节信息。我们下一节也会基于这几种方式来自定义错误类型。

不要忽略错误

注意,永远不要忽略错误字段。比如下面的程序就忽略了filepath.Glob()返回的错误字段。

package main

import (
    "fmt"
    "path/filepath"
)

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

由于入参[是格式错误的,因此会返回错误字段,但是我们通过_将该错误忽略了。该程序输出为matched files [],表面上看起来是未匹配到任何字段,其实是发生了错误。因此我们在Golang编程中永远不要随意的忽略一个错误。

Last updated