在实际工程项目中,总是通过程序的错误信息快速定位问题,但是又不希望错误处理代码写的冗余而又啰嗦。Go语言没有提供像Java、C#语言中的try…catch异常处理方式,而是通过函数返回值逐层往上抛。这种设计,鼓励在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码冗余。
什么是错误
错误指的是可能出现问题的地方出现了问题。如打开一个文件时失败,这种情况是在意料之中的 。
而异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在在意料之外的。可见,错误是业务过程的一部分,而异常不是 。
Go中的错误也是一种类型。错误用内置的error 类型表示。就像其他类型的,如int,float64,。错误值可以存储在变量中,从函数中返回,等等。
演示错误
尝试打开一个不存在的文件:
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值表示出现错误。
运行结果:
open /test.txt: No such file or directory
得到一个错误,说明该文件不存在。
错误类型表示
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
它非常简单,只有一个 Error 方法用来返回具体的错误信息:
type error interface {
Error() string
}
它包含一个带有Error()字符串的方法。任何实现这个接口的类型都可以作为一个错误使用。这个方法提供了对错误的描述。
当打印错误时,fmt.Println函数在内部调用Error() 方法来获取错误的描述。这就是错误描述是如何在一行中打印出来的。
从错误中提取更多信息的不同方法
在上面的例子中,仅仅是打印了错误的描述。如果想要的是导致错误的文件的实际路径。一种可能的方法是解析错误字符串,
open /test.txt: No such file or directory
可以解析这个错误消息并从中获取文件路径"/test.txt"。但这是一个糟糕的方法。在新版本的语言中,错误描述可以随时更改,代码将会中断。
标准Go库使用不同的方式提供更多关于错误的信息。
断言底层结构类型,结构字段获取
如果仔细阅读打开函数的文档,可以看到它返回的是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结构的路径字段包含导致错误的文件的路径。修改上面示例,并打印出路径:
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类型的程序,并调用这些方法来确定错误是临时的还是超时的。
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地址,通过声明它来输入net.DNSError来获得错误的潜在价值。
在例子中,错误既不是暂时的,也不是由于超时,因此程序会打印:
generic error: lookup golangbot123.com: no such host
如果错误是临时的或超时的,那么相应的If语句就会执行,可以适当地处理它。
直接比较
获得更多关于错误的详细信息的第三种方法是直接与类型错误的变量进行比较。
filepath包的Glob函数用于返回与模式匹配的所有文件的名称。当模式出现错误时,该函数将返回一个错误ErrBadPattern。
在filepath包中定义了ErrBadPattern,如下所述:
var ErrBadPattern = errors.New(“syntax error in pattern”)
errors.New()用于创建新的错误。
当模式出现错误时,由Glob函数返回ErrBadPattern:
func main() {
files, error := filepath.Glob("[")
if error != nil && error == filepath.ErrBadPattern {
fmt.Println(error)
return
}
fmt.Println(“matched files”, files)
}
运行结果:
syntax error in pattern
不要忽略错误
永远不要忽略一个错误。忽视错误会招致麻烦。下面一个示例,列出了与模式匹配的所有文件的名称,而忽略了错误处理代码。
func main() {
files, _ := filepath.Glob("[")
fmt.Println(“matched files”, files)
}
使用行号中的空白标识符,忽略了Glob函数返回的错误:
matched files []
由于忽略了这个错误,输出看起来好像没有文件匹配到这个模式,但是实际上这个模式本身是畸形的。所以不要忽略错误。
自定义错误
创建自定义错误可以使用errors包下的New()函数,以及fmt包下的:Errorf()函数。
//errors包:
func New(text string) error {}
//fmt包:
func Errorf(format string, a …interface{}) error {}
下面提供了错误包中的新功能的实现。
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
既然知道了New()函数是如何工作的,那么就使用它来创建一个自定义错误。
创建一个简单的程序,计算一个圆的面积,如果半径为负,将返回一个错误。
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New(“Area calculation failed, radius is less than zero”)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf(“Area of circle %0.2f”, area)
}
运行结果:
Area calculation failed, radius is less than zero
使用Errorf向错误添加更多信息
上面的程序运行没有问题,但是如果要打印出导致错误的实际半径,就不好处理了。这就是fmt包的Errorf函数的用武之地。这个函数根据一个格式说明器格式化错误,并返回一个字符串作为值来满足错误。
使用Errorf函数,修改程序:
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf(“Area calculation failed, radius %0.2f is less than zero”, radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf(“Area of circle %0.2f”, area)
}
运行结果:
Area calculation failed, radius -20.00 is less than zero
使用结构体和字段提供错误的更多信息
还可以使用将错误接口实现为错误的struct类型。这使得错误处理更加的灵活。在上述示例中,如果想要访问导致错误的半径,那么唯一的方法是解析错误描述区域计算失败,半径-20.00小于零。这不是一种正确的方法,因为如果描述发生了变化,那么代码就会中断。
前面提到“断言底层结构类型从struct字段获取更多信息”,并使用struct字段来提供对导致错误的半径的访问。可以创建一个实现错误接口的struct类型,并使用它的字段来提供关于错误的更多信息。
1、创建一个struct类型来表示错误。错误类型的命名约定是,名称应该以文本Error结束:
type areaError struct {
err string
radius float64
}
上面的struct类型有一个字段半径,它存储了为错误负责的半径的值,并且错误字段存储了实际的错误消息。
2、实现error 接口
func (e *areaError) Error() string {
return fmt.Sprintf(“radius %0.2f: %s”, e.radius, e.err)
}
在上面的代码片段中,使用一个指针接收器区域错误来实现错误接口的Error() string方法。这个方法打印出半径和错误描述。
type areaError struct {
err string
radius float64
}
func (e *areaError) Error() string {
return fmt.Sprintf(“radius %0.2f: %s”, e.radius, e.err)
}
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{“radius is negative”, radius}
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
if err, ok := err.(*areaError); ok {
fmt.Printf(“Radius %0.2f is less than zero”, err.radius)
return
}
fmt.Println(err)
return
}
fmt.Printf(“Area of circle %0.2f”, area)
}
程序输出:
Radius -20.00 is less than zero
使用结构体方法提供错误的更多信息
1、创建一个结构来表示错误。
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
上面的错误结构类型包含一个错误描述字段,以及导致错误的长度和宽度。
2、实现错误接口,并在错误类型上添加一些方法来提供关于错误的更多信息。
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
在上面的代码片段中,返回Error() string 方法的错误描述。当长度小于0时,lengthNegative() bool方法返回true;当宽度小于0时,widthNegative() bool方法返回true。这两种方法提供了更多关于误差的信息。
面积计算函数:
func rectArea(length, width float64) (float64, error) {
err := “”
if length < 0 {
err += “length is less than zero”
}
if width < 0 {
if err == “” {
err = “width is less than zero”
} else {
err += “, width is less than zero”
}
}
if err != “” {
return 0, &areaError{err, length, width}
}
return length * width, nil
}
上面的rectArea函数检查长度或宽度是否小于0,如果它返回一个错误消息,则返回矩形的面积为nil。
主函数:
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Printf(“error: length %0.2f is less than zero\n”, err.length)
}
if err.widthNegative() {
fmt.Printf("error: width %0.2f is less than zero\n", err.width)
}
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
运行结果:
error: length -5.00 is less than zero
error: width -9.00 is less than zero
错误断言
有了自定义的 error,并且携带了更多的错误信息后,就可以使用这些信息了。需要先把返回的 error 接口转换为自定义的错误类型,即类型断言。
下面代码中的 err.(*commonError) 就是类型断言在 error 接口上的应用,也可以称为 error 断言。
sum, err := add(-1, 2)
if cm,ok := err.(*commonError);ok{
fmt.Println(“错误代码为:”,cm.errorCode,",错误信息为:",cm.errorMsg)
} else {
fmt.Println(sum)
}
如果返回的 ok 为 true,说明 error 断言成功,正确返回了 *commonError 类型的变量 cm,所以就可以像示例中一样使用变量 cm 的 errorCode 和 errorMsg 字段信息了。
参考资料:
小海鲸 https://www.xiao-haijing.com