error类型是一个接口类型,是Go语言內建类型,在这个接口类型的声明中只包含一个方法Error
,这个方法不接受任何参数,但是会返回一个string类型的结果。
type error interface {
Error() string //返回错误信息的字符串形式
}
使用error类型的方式,通常是在函数声明的结果列表的最后,声明一个error类型的结果,同时在调用这个函数之后,先判断它返回的最后一个结果值是否“不为nil”。
package main
import (
"errors"
"fmt"
)
func echo(request string) (response string, err error) {
if request == "" { // 卫述语句
err = errors.New("empty request")
return
}
response = fmt.Sprintf("echo: %s", request)
return
}
func main() {
for _, req := range []string{"", "hello!"} {
fmt.Printf("request: %s\n", req)
resp, err := echo(req)
if err != nil { // 卫述语句
fmt.Printf("error: %s\n", err)
continue
}
fmt.Printf("response: %s\n", resp)
}
}
注意点:
errors.New
函数,这是一种最基本的生成错误值的方式。调用它时传入一个有字符串代表的错误信息,返回一个包含这个错误信息的error
类型值。该值的静态类型是error
,动态类型是一个errors
包中,包级私有的类型*errorString
。
errorString
类型拥有的一个指针方法实现了errors
接口中的Error
方法。这个方法被调用后,会原封不动地返回之前传入的错误信息,实际上,error
类型值的Error
方法就相当于其他类型值的String
方法。
在上述例子中,fmt.Printf
函数发现被打印的是一个error
类型,就会调用它的Error
方法。在fmt
包中,这类打印函数其实都是这么做的。
当我们通过模板化的方式生成错误信息,并得到错误值时,可以使用fmt.Errorf
函数,该函数其实是先调用fmt.Sprintf
函数,得到确切的错误信息,在调用errors.New
函数,得到该错误信息的error
类型值,最后返回该值。
因为error
是一个接口类型,所以即使同为error
类型的错误值,他们的实际类型也可能不同。
类型在已知范围内的错误值是最容易分辨的。如os包中的几个代表错误类型:
os.PathError
os.LinkError
os.SyccallError
os/exec.Error
它们的指针类型都是error接口的实现类型,同时它们也都包含了一个名叫Err
,类型为error
接口类型的代表潜在错误的字段。如果得到一个error
类型值,并且知道该值的实际类型肯定是它们中的某一个,那么就用switch语句去判断:
func underlyingError(err error) error {
switch err := err.(type) {
case *os.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
case *exec.Error:
return err.Err
}
return err
}
// 只要类型不同,就可以使用这种方式来判断,但是如果错误值类型相同,那么这种方式就无效了
如os包中不少错误类型都是通过调用errors.New
函数来初始化:
os.ErrClosed
os.ErrInvalid
os.ErrPermission
这几个都是已经定义好的,确切的错误值。os包中的代码有时候会把他们当做潜在的错误值,封装进前面那些错误类型的值中。
如果在操作文件系统的时候得到一个错误值,并且知道该值的潜在错误值肯定是上述值中的某一个,那就可以用普通的switch或者if和判等语句去做判断:
printError := func(i int, err error) { // 接受error类型的参数值,该值代表某个文件操作相关的错误
if err == nil {
fmt.Println("nil error")
return
}
err = underlyingError(err)
switch err {
case os.ErrClosed:
fmt.Printf("error(closed)[%d]: %s\n", i, err)
case os.ErrInvalid:
fmt.Printf("error(invalid)[%d]: %s\n", i, err)
case os.ErrPermission:
fmt.Printf("error(permission)[%d]: %s\n", i, err)
}
}
// 虽然不知道这些错误值的类型范围,但却知道它们或它们潜在的错误值一点在某个已知的os包中定义的值
如果对于一个错误值可能代表的含义知之甚少,那么只能通过它拥有的错误信息去判断了。
我们总是能通过错误值的Error方法拿到它的错误信息。在os包中有os.IsExit
、os.IsNotExit
和os.IsPermission
函数来判断。
构建错误值体系的基本方式有两种:
在Go语言中实现接口都是非侵入式的,所以可以做的非常灵活。
比如在net包中,有一个名为Error的接口类型,它算是內建接口类型error的一个扩展接口,因为error是net.Error
的嵌入接口。
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
net包中有很多错误类型都实现了net.Error
接口,如:
*net.OpError
*net.AddrError
net.UnknownNetworkError
把错误类型想象成一棵树,内建接口error就是树根,net.Error
接口就是一个在根上延伸的第一级非叶子节点。
用类型建立起树形结构的错误体系,用统一字段建立起可追根溯源的链式错误关联。
如果不希望包外代码改动返回错误值的话,一定要小写其中字段的名称首字母。通过暴露某些方法让包外代码有进一步获取错误信息的权限,比如编写一个可以返回包级私有的err字段值的公开方法Err。
相对于立体的错误类型体系,扁平的错误列表值简单很多。只是想要先创建一些代表已知错误的错误值的时候,用扁平化的方式很恰当。