error 接口申明
type error interface {
Error() string
}
比较 error 对象
因为 error 是一个接口对象,零值是 nil,所以直接和 nil 比较判定错误:
func main() {
_, err := os.Open("/tmp/file.txt")
if err != nil {
// open file faile, err: open /tmp/file.txt: no such file or directory
fmt.Println("open file faile, err: ", err)
return
}
}
代码的主要作用是尝试打开一个名为/tmp/file.txt的文件。它使用了os包中的Open()函数来打开文件,该函数返回两个值:一个*File类型的文件指针和一个error类型的错误值。
代码首先尝试打开/tmp/file.txt文件,并将文件指针和错误值分别赋值给_和err变量。由于代码使用了_变量,这意味着它忽略了Open()函数返回的文件指针,也就是说,它不需要访问该文件,只是想要检查是否成功打开该文件。
然后,代码使用了一个条件语句来检查错误值是否为nil。如果不是nil,说明打开文件时发生了错误,此时代码将输出一条错误消息并返回。错误消息中包含了具体的错误信息,其中err.Error()函数返回的是error类型值的字符串表示。
创建错误
// io.EOF
var EOF = errors.New("EOF")
// 自定义错误类型
var ErrInvalid = errors.New("invalid operator")
代码定义了两个错误变量EOF和ErrInvalid,它们分别表示不同类型的错误。
EOF代表了io包中定义的错误类型,它表示在读取输入流时已经到达末尾。在io包中,该错误变量通常用于指示读取操作是否已经完成。
ErrInvalid是开发者自定义的一个错误类型,它表示传递给程序的操作符不合法,例如,程序只能接受+和-操作符,而传递了*操作符。在这种情况下,程序可以返回该错误类型来指示输入错误。
这两个错误变量都是使用errors.New()函数创建的。errors.New()函数接受一个字符串参数,并将其作为错误消息创建一个新的error类型值。该函数返回的值可以用于表示各种类型的错误。
包装错误
func main() {
var err = errors.New("invalid operator")
// unexpected, err: invalid operator
err1 := fmt.Errorf("unexpected, err: %v", err)
fmt.Println(err1)
// unknown, err: unexpected, err: invalid operator
err2 := fmt.Errorf("unknown, err: %v", err1)
fmt.Println(err2)
}
代码定义了一个错误变量err,并使用它来创建两个新的错误变量err1和err2。
代码首先创建了一个值为"invalid operator"的错误变量err。然后,它使用fmt.Errorf()函数创建了一个新的错误变量err1,该错误变量的错误消息为"unexpected, err: invalid operator"。在错误消息中,%v占位符表示将err的值格式化为字符串后插入该位置。因此,err1的错误消息将包含原始错误变量err的字符串表示。
接下来,代码使用fmt.Errorf()函数再次创建了一个新的错误变量err2,该错误变量的错误消息为"unknown, err: unexpected, err: invalid operator"。在这个错误消息中,%v占位符表示将err1的值格式化为字符串后插入该位置。因此,err2的错误消息将包含上一个错误变量err1的字符串表示。
errors 包提供的诊断方法
// 去掉一层 err 的包装
func Unwrap(err error) error
// 是否包含 target 错误类型
func Is(err, target error) bool
// 是否为 target 类型
func As(err error, target interface{}) bool
把 err 错误都导出
对于调用者,如何准确的得知是哪种错误呢? 对错误文本进行比较显然比较繁琐,通常的办法是把错误类型都导出,让包外的调用者直接使用:
// 假设这是 userservice 包内的错误
type UserService struct{}
var ErrInvalidUserID = errors.New("invalid user id")
var ErrPending = errors.New("not implement")
func (svc *UserService) GetUserByID(ID string) error {
if len(ID) == 0 {
return ErrInvalidUserID
}
return ErrPending
}
代码定义了一个UserService类型和两个错误变量ErrInvalidUserID和ErrPending,同时还实现了一个GetUserByID()方法,用于根据用户ID获取用户信息。
在GetUserByID()方法中,代码首先检查传入的用户ID是否为空。如果是空字符串,则返回ErrInvalidUserID错误变量,表示传入的用户ID无效。否则,代码返回ErrPending错误变量,表示该方法还没有实现。
需要注意的是,这里的错误处理机制是将错误作为函数的返回值进行处理。如果函数执行成功,它将返回nil值;否则,它将返回一个非nil值,该值是一个error类型的变量,用于表示具体的错误信息。在这里,GetUserByID()方法只是简单地返回一个错误变量,而没有进行任何更深入的错误处理。
使用 Must 风格函数诊断
有时候有些 error 会直接导致程序无法运行,所以会写出:
func connect() {
if err != nil {
panic(err)
}
}
转换成 Must 风格的函数会比较明显,常用于测试、DB 连接等场景:
type Resource struct{}
func connect() (*Resource, error) {
// try to provide a resource
res := &Resource{}
// but a error happened
err := errors.New("mock error")
return res, err
}
func MustConnect() *Resource {
res, err := connect()
if err != nil {
panic(err)
}
return res
}
func main() {
// res, err := connect()
// if err != nil {
// panic(err)
// }
res := MustConnect()
if res != nil {
log.Println("get a resource")
}
}
代码定义了一个Resource类型和两个函数connect()和MustConnect(),并在main()函数中使用MustConnect()函数来获取一个资源。
connect()函数模拟了一个连接资源的操作。在函数内部,代码尝试创建一个新的Resource类型的实例,并返回该实例的指针。同时,代码也创建了一个错误变量err,表示在连接资源时发生了错误。实际开发中,错误变量可能会包含更加详细和准确的错误信息。
MustConnect()函数是一个辅助函数,它用于在连接资源失败时引发一个运行时错误(panic)。在函数内部,代码首先调用connect()函数来获取一个资源,并将资源指针和错误变量分别赋值给res和err变量。然后,代码检查错误变量是否为nil。如果不是nil,则使用panic()函数来引发一个运行时错误,并将错误变量作为参数传递给panic()函数。否则,代码返回资源指针res。
在main()函数中,代码使用MustConnect()函数来获取一个资源,并检查资源指针是否为nil。如果资源指针不为空,代码将输出一条日志消息,表示已经成功获取了一个资源。如果资源指针为空,则说明在获取资源时发生了错误,并且该错误已经被MustConnect()函数捕获并转换为一个运行时错误。
使用 pkg errors 包
社区里 errors 包已经是事实上的标准,提供了更合理的 wrap/unwrap/cause/is/as 等诊断函数,项目中通常不会用原始的标准库的 errors 包。
go get -u github.com/pkg/errors
func fn() error {
e1 := errors.New("root error")
e2 := errors.Wrap(e1, "inner")
e3 := errors.Wrap(e2, "middle")
return errors.Wrap(e3, "outer")
}
func main() {
err := fn()
// outer: middle: inner: root error
fmt.Println(err)
// root error
fmt.Println(errors.Cause(err))
}
自定义 error 结构体对象
由于 error 对象只是实现了 Error() string 函数的结构,因此任何实现了该接口的对象都是 error 对象:
type InvalidError struct {
Message string
Extend string
}
// Error OpError 类型实现error接口
func (e *InvalidError) Error() string {
return fmt.Sprintf("无效的数据: %s", e.Message)
}
func (e *InvalidError) Detail() string {
return fmt.Sprintf("具体的要求: %s", e.Extend)
}
func fn() error {
return &InvalidError{Message: "格式不对", Extend: "需要11位手机号"}
}
func main() {
err := fn()
fmt.Println(err)
initialErr, ok := err.(*InvalidError)
if ok {
fmt.Println(initialErr.Detail())
}
}
代码定义了一个InvalidError类型和三个方法:Error()、Detail()和fn()。其中,InvalidError类型用于表示一个无效数据的错误,该错误包含一个Message字段和一个Extend字段,分别表示错误的概要和具体的要求。Error()方法用于实现error接口,它返回一个字符串,表示该错误的概要信息。Detail()方法返回一个字符串,表示该错误的具体要求。fn()函数模拟了一个返回错误的操作,它返回一个InvalidError类型的错误值。
在main()函数中,代码调用fn()函数来获取一个错误值,并使用fmt.Println()函数输出该错误值。由于InvalidError类型实现了error接口,因此fmt.Println()函数会自动调用该错误值的Error()方法,并将其返回的字符串作为输出内容。因此,输出的字符串将包含错误的概要信息。
然后,代码使用类型断言的方式将错误值转换为InvalidError类型,并将转换后的值赋值给initialErr变量。如果类型断言成功,则说明该错误确实是InvalidError类型的错误,此时代码调用initialErr的Detail()方法来获取错误的具体要求,并使用fmt.Println()函数输出该要求。
需要注意的是,这里使用了类型断言的方式来判断错误类型,并获取特定类型的错误值。在实际开发中,为了避免类型转换带来的不必要的开销和风险,应该尽可能地使用类型断言来处理错误类型。
省略多余的 error 判断
由于 error 检查在 golang 里很普遍,有时候会写出多余的 if 判断:
func fn() error {
return errors.New("mock error")
}
func do1() error {
// ...
if err := fn(); err != nil {
return err
}
return nil
}
func do2() error {
// ...
return fn()
}