error 接口申明

type error interface {
  Error() string
}

比较 error 对象

因为 error 是一个接口对象,零值是 nil,所以直接和 nil 比较判定错误:

在线运行open in new window启动 AI 助手open in new window

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类型值。该函数返回的值可以用于表示各种类型的错误。

包装错误

在线运行open in new window启动 AI 助手open in new window

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 错误都导出

对于调用者,如何准确的得知是哪种错误呢? 对错误文本进行比较显然比较繁琐,通常的办法是把错误类型都导出,让包外的调用者直接使用:

在线运行open in new window启动 AI 助手open in new window

// 假设这是 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 连接等场景:

在线运行open in new window启动 AI 助手open in new window

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()函数捕获并转换为一个运行时错误。

sqlx 的 MustConnect 源代码open in new window

使用 pkg errors 包

社区里 errors 包已经是事实上的标准,提供了更合理的 wrap/unwrap/cause/is/as 等诊断函数,项目中通常不会用原始的标准库的 errors 包。

go get -u github.com/pkg/errors

在线运行open in new window启动 AI 助手open in new window

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 对象:

在线运行open in new window启动 AI 助手open in new window

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 判断:

在线运行open in new window启动 AI 助手open in new window

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()
}
Last Updated:
Contributors: Bob Wang