Go 1.13 如何优雅的处理错误

276 阅读3分钟

Go 1.13

Go 1.13 的发布为错误处理进带来了全新功能 Error wrapping

具体细节可以参考相关提案 Proposal: Go 2 Error Inspection 以及 issues/29934 讨论。

Go 1.13 为 errorsfmt 标准库包引入了新功能:

  • fmt.Errorf 支持使用 %w 动词包装错误。
  • 新增 errors.Unwrap 函数为错误解包以获取根因。
  • 新增 errors.Is 函数取代 == 做等值比较。
  • 新增 errors.As 函数取代 Type Assertion 做类型断言。

我们来一一讲解。

fmt.Errorf

fmt.Errorf 新增的 %w 动词功能对标 pkg/errors 包中的 errors.Wrap 函数,用法如下:

package main

import (
	"errors"
	"fmt"
)

func Foo() error {
	return errors.New("foo error")
}

func Bar() error {
	err := Foo()
	if err != nil {
		return fmt.Errorf("bar: %w", err)
	}
	return nil
}

func main() {
	err := Bar()
	if err != nil {
		fmt.Printf("err: %s\n", err)
	}
}

执行示例代码,得到输出如下:

$ go run main.go
err: bar: foo error
errors.Unwrap

errors.Unwrap 函数对标 pkg/errors 包中的 errors.Cause 函数,用法如下:

func Foo() error {
	return io.EOF
}

func Bar() error {
	err := Foo()
	if err != nil {
		return fmt.Errorf("bar: %w", err)
	}
	return nil
}

func main() {
	err := Bar()
	if err != nil {
		if errors.Unwrap(err) == io.EOF {
			fmt.Println("EOF err")
			return
		}
		fmt.Printf("err: %+v\n", err)
	}
	return
}

执行示例代码,得到输出如下:

$ go run main.go
EOF err
errors.Is

errors.Is 函数可以取代 == 做等值比较,用法如下:

func Foo() error {
	return io.EOF
}

func Bar() error {
	err := Foo()
	if err != nil {
		return fmt.Errorf("bar: %w", err)
	}
	return nil
}

func main() {
	err := Bar()
	if err != nil {
		// if err == io.EOF {
		if errors.Is(err, io.EOF) {
			fmt.Println("EOF err")
			return
		}
		fmt.Printf("err: %+v\n", err)
	}
	return
}

执行示例代码,得到输出如下:

$ go run main.go
EOF err

由于我们使用了 fmt.Errorf("bar: %w", err) 对初始错误进行了包装,所以在 main 函数中不能直接使用 if err == io.EOF 来对 Sentinel error 进行相等性判断。

除了可以使用 if errors.Unwrap(err) == io.EOF 这种方式,我们还可以使用 errors.Is(err, io.EOF) 方式来取代 ==,执行结果相同。

errors.As

errors.As 函数可以取代 Type Assertion 做类型断言,用法如下:

type MyError struct {
	msg string
	err error
}

func (e *MyError) Error() string {
	return e.msg + ": " + e.err.Error()
}

func Foo() error {
	return &MyError{
		msg: "foo",
		err: io.EOF,
	}
}

func Bar() error {
	err := Foo()
	if err != nil {
		return fmt.Errorf("bar: %w", err)
	}
	return nil
}

func main() {
	err := Bar()
	if err != nil {
		var e *MyError
		if errors.As(err, &e) {
			fmt.Printf("EOF err: %s\n", e)
			return
		}
		fmt.Printf("err: %+v\n", err)
	}
	return
}

执行示例代码,得到输出如下:

$ go run main.go
EOF err: foo: EOF

Go 1.20

Go 1.20 新增了 errors.Join 函数返回包装后的错误列表。

用法如下:

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("err1")
	err2 := errors.New("err2")
	err := errors.Join(err1, err2)
	fmt.Println("---------")
	fmt.Println(err)
	fmt.Println("---------")
	if errors.Is(err, err1) {
		fmt.Println("err is err1")
	}
	if errors.Is(err, err2) {
		fmt.Println("err is err2")
	}
}

执行示例代码,得到输出如下:

$ go run main.go
---------
err1
err2
---------
err is err1
err is err2

可以发现通过 errors.Join(err1, err2) 得到的 err 对象打印结果中会在多个错误之间增加换行。并且 errors.Is 函数也做了升级,能够处理这种情况,对 err1err2 的相等性比较结果都为 true

Go 1.23

Go 1.23errors.Is 函数做了一点小优化。

这是 Go 1.23 中的 errors.Is 函数:

github.com/golang/go/b…

func Is(err, target error) bool {
	if err == nil || target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	return is(err, target, isComparable)
}

这是 Go 1.22.8 中的 errors.Is 函数:

github.com/golang/go/b…

func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	return is(err, target, isComparable)
}

可以发现 Go 1.23 中 errors.Is 函数多了一个 if err == nil 逻辑的判断。不过由于这个改动比较小,不会对现有代码造成任何影响,这一改变并没有出现在 Go 1.23 Release Notes 中。

仅此而已。

Go 2

Go 1.13 版本之前的错误处理是过去,Go 1.13 版本错误处理是现在,Go 2 版本错误处理是未来。

由于社区中对 Go 错误处理的吐槽声一直不断,Go 也在努力改变这一现状,虽然进展缓慢。

Go 1.13 的出现已经是最大的诚意了 :)。

很多人依然不满足于现状,将错误处理希望寄托于 Go 2。不过我个人持悲观态度,基于 Go 泛型的演进过程,我认为几年内 Go 错误处理都不会有较大改变。

不过 Go 2 的蓝图的确已经在勾勒中了,感兴趣的读者可以进入 Go 2 Draft Designs 查看,里面对 Error handling 以及 Error values 都罗列了几个链接供读者参阅。

期待 Go 2 的错误处理能够更上一层楼。

总结

Go 1.13 虽然新增了几个方法,用来辅助错误处理,但是依然不能记录堆栈信息。

所以,即使有 Go 1.13 的存在,现在也依然推荐使用 pkg/errors 来处理错误。

Go 2 承担了 Go 未来错误处理的重任。

本文示例源码我都放在了 GitHub 中,欢迎点击查看。

希望此文能对你有所启发。

联系我