在实际工程项目中,我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。Go
语言没有提供像Java
、C#
语言中的try...catch
异常处理方式,而是通过函数返回值逐层往上抛。这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码啰嗦。
1.1 什么是错误
错误是什么?
错误指的是可能出现问题的地方出现了问题。比如打开一个文件时失败,这种情况在人们的意料之中 。
而异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。
Go中的错误也是一种类型。错误用内置的error
类型表示。就像其他类型的,如int,float64,。错误值可以存储在变量中,从函数中返回,等等。
1.2 演示错误
让我们从一个示例程序开始,这个程序尝试打开一个不存在的文件。
示例代码:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
//根据f进行文件的读或写
fmt.Println(f.Name(), "opened successfully")
}
在os包中有打开文件的功能函数:
func Open(name string) (file *File, err error)
如果文件已经成功打开,那么Open函数将返回文件处理。如果在打开文件时出现错误,将返回一个非nil错误。
如果一个函数或方法返回一个错误,那么按照惯例,它必须是函数返回的最后一个值。因此,Open
函数返回的值是最后一个值。
处理错误的惯用方法是将返回的错误与nil进行比较。nil值表示没有发生错误,而非nil值表示出现错误。在我们的例子中,我们检查错误是否为nil。如果它不是nil,我们只需打印错误并从主函数返回。
运行结果:
open /test.txt: No such file or directory
我们得到一个错误,说明该文件不存在。
1.3 错误类型表示
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
让我们再深入一点,看看如何定义错误类型的构建。错误是一个带有以下定义的接口类型,
type error interface {
Error() string
}
它包含一个带有Error()字符串的方法。任何实现这个接口的类型都可以作为一个错误使用。这个方法提供了对错误的描述。
当打印错误时,fmt.Println函数在内部调用Error() 方法来获取错误的描述。这就是错误描述是如何在一行中打印出来的。
从错误中提取更多信息的不同方法
既然我们知道错误是一种接口类型,那么让我们看看如何提取更多关于错误的信息。
在上面的例子中,我们仅仅是打印了错误的描述。如果我们想要的是导致错误的文件的实际路径。一种可能的方法是解析错误字符串。这是我们程序的输出,
open /test.txt: No such file or directory
我们可以解析这个错误消息并从中获取文件路径"/test.txt"。但这是一个糟糕的方法。在新版本的语言中,错误描述可以随时更改,我们的代码将会中断。
是否有办法可靠地获取文件名?答案是肯定的,它可以做到,标准Go库使用不同的方式提供更多关于错误的信息。让我们一看一看。
1.断言底层结构类型并从结构字段获取更多信息
如果仔细阅读打开函数的文档,可以看到它返回的是PathError类型的错误。PathError是一个struct类型,它在标准库中的实现如下,
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
从上面的代码中,您可以理解PathError通过声明Error()string
方法实现了错误接口。该方法连接操作、路径和实际错误并返回它。这样我们就得到了错误信息,
open /test.txt: No such file or directory
PathError结构的路径字段包含导致错误的文件的路径。让我们修改上面写的程序,并打印出路径。
修改代码:
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")
}
在上面的程序中,我们使用类型断言获得错误接口的基本值。然后我们用错误来打印路径.这个程序输出,
File at path /test.txt failed to open
- 断言底层结构类型,并使用方法获取更多信息
获得更多信息的第二种方法是断言底层类型,并通过调用struct类型的方法获取更多信息。
示例代码:
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}
从上面的代码中可以看到,DNSError struct有两个方法Timeout() bool和Temporary() 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)
}
在上面的程序中,我们正在尝试获取一个无效域名的ip地址,这是一个无效的域名。http://golangbot123.com。我们通过声明它来输入*net.DNSError来获得错误的潜在价值。
在我们的例子中,错误既不是暂时的&#x