Error Handling
本文是《Go系列教程》的第二十八篇文章。
什么是错误?
错误即是表明了在程序中发生了异常情况。比如说,我们试图打开一个文件,但这个文件并不在文件系统中。这就是一个异常情况,我们就用一个error来表示它。
错误都是使用一个内置的error类型来表示的。后面我们还会讲更多关于error类型的技术。
和其他内置类型int、float64等一样,错误值可以被存储到变量中,作为参数传递给函数,以及从函数中返回等等。
示例
我们直接用个示例程序来演示一下。在这个程序中,我们试图打开一个并不存在的文件。
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”的文件(此文件根本不存在)。其中Open函数的方法签名如下:
func Open(name string) (file *File,err error)
如果文件被成功打开,Open函数就将返回此文件的句柄,以及值为nil的错误。如果在打开文件时,发生错误的话,就会返回一个non-nil的error。
如果函数或方法返回一个错误的话,那么根据约定,它必须是此方法返回值的最后的那个值。因此,Open函数就把返回的err作为最后的值。
Go处理错误的管用方法是:判断返回的error值是否为nil。值为nil就表明了没有发生异常,非nil值就表明了出现了一个错误。在我们的案例中,我们会检查error的值是否是nil。如果它不是nil的话,我们就把错误信息打印出来,并直接返回。
运行此程序,将得到如下输出:
open /test.txt: No such file or directory
错误类型的表示
我们来再挖深一点,看一下内置的error类型是如何定义的。error是一个接口类型,其定义如下:
type error interface {
Error() string
}
它包含了一个签名为Error() string 的方法。任何实现了此接口的类都可以作为error使用。该方法提供了错误的描述信息。
当打印此错误的时候,fmt.Println函数内部调用了Error() string方法去获取错误的描述信息。
从错误中提取信息的多种不同方式
到目前为止,我们已经知道了,error是一个接口类型,我们来看一下我们如何提取关于此错误的更多信息。
在上面的案例中,我们打印出了错误的描述信息。如果我们想要获取到此文件的真实路径的话,一个可能的方式就是,转换此错误字符串。
我们的程序的输出:
open /test.txt: No such file or directory
我们可以转换此错误消息,并获取到导致此错误的“/test.txt”文件的路径。但是这是一种糟糕的方式。因为错误的描述在新版的Go中可能会发生变化,这样的话,我们的代码就可能会挂掉。
既然如此,那么,有没有一种更好地方式获取文件名呢?答案是:有。Go的标准库采用不同的方式提供了关于错误的更多信息。我们来一个个看下。
1.判断底层的数据类型并从数据结构的域字段中获取更多信息
如果你仔细阅读Open函数的文档的话,你会发现,它返回的错误是*PathError类型。PathError是一种结构体类型,其在标准库中的实现如下所示:
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
从上述的代码中,你可以理解为*PathError实现了error接口。并声明了一个Error() string方法。此函数把操作,路径、真实的错误信息连接在一起,并返回。
因此,我们得到的错误信息为:
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 != nil {
if pErr, ok := err.(*os.PathError); ok {
fmt.Println("Failed to open file at path", pErr.Path)
return
}
fmt.Println("Generic error", err)
return
}
fmt.Println(f.Name(), "opened successfully")
}
程序的输出如下:
Failed to open file at path /test.txt
2.判断底层的数据类型并使用方法来获取更多错误信息
第二种获取错误信息的方式是:判断底层的类型,并通过调用结构体上的对应方法来获取更多错误信息。
我们来通过一个例子来理解下。
标准库中的DNSError结构体类型的定义如下:
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}
DNSError结构体中有俩个方法:Timeout() bool 和 Temporary() bool,这俩个函数都会返回一个布尔值来表明此错误是一个超时错误还是一个临时错误。
我们来写个程序。
package main
import (
"fmt"
"net"
)
func main() {
addr, err := net.LookupHost("golangbot123.com")
if err != nil {
if dnsErr, ok := err.(*net.DNSError); ok {
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)
}
在上面的程序中,我们会试图获取一个无效域名golangbot123.com的ip地址。我们通过判断它是否是*net.DNSError来获取错误的底层的值,之后,我们检查此错误是由于超时造成的还是临时性的。
在我们的案例中,错误要么是temporary错误要么是timeout错误,因此程序将输出如下:
Generic DNS error lookup golangbot123.com: no such host
如果错误是临时性的或是由于超时造成的,那么就会执行相应的语句,我就可以正确地处理它。
3.直接比较
获取错误信息的第三种方式就是直接和一个error类型的变量比较。我们来写个例子理解下。
Glob函数可返回所有匹配指定模式的所有文件名。当模式有问题的时候,此函数会返回一个ErrBadPattern错误。
ErrBadPattern是作为全局变量定义在filepath包中。
var ErrBadPattern = errors.New("syntax error in pattern")
errors.New()可用于创建一个新的错误,我们会在下一篇中再详细讲述。
当给定的模式不正确时,Glob函数会返回一个ErrBadPattern错误。
我们来写段程序理解下。
package main
import (
"fmt"
"path/filepath"
)
func main() {
files, err := filepath.Glob("[")
if err != nil {
if err == filepath.ErrBadPattern {
fmt.Println("Bad pattern error:", err)
return
}
fmt.Println("Generic error:", err)
return
}
fmt.Println("matched files", files)
}
在上面的程序中,我们寻找所有匹配模式“[”的文件。我们首先检查error是否为Nil。为了得到更多错误信息,我们直接拿它和filepath.ErrBadPattern相比较。如果条件满足的话,即错误是由于不正确的模式造成的,程序就会输出:
Bad pattern error: syntax error in pattern
标准库可以使用上述的任意一种方式获取更多的错误信息。我们将在下一篇文章中使用这些方式创建我们自己的错误。
不用忽略错误
永远不要忽略一个错误。忽视错误的话,会带来一些问题。我们来重新上面的案例,并忽略错误处理代码。
package main
import (
"fmt"
"path/filepath"
)
func main() {
files, _ := filepath.Glob("[")
fmt.Println("matched files", files)
}
我们从上个例子就已经知道了,我们在Glob中指定的模式是无效的。在这里我使用"_"符号,忽略了Glob函数返回的错误。
我们简单地打印了所匹配的文件,程序的输出如下:
matched files []
由于我们忽略了该错误,所以程序的输出看起来仿佛没有文件匹配此模式一样。但其实该模式本身就是有问题的。因此,永远不要忽略错误。
感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!
备注
本文系翻译之作原文博客地址
网友评论