2

卷起来,老程序员也得了解errors包的新变化

 9 months ago
source link: https://colobu.com/2023/12/13/learn-more-about-errors/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go 1.13 中errors包有了一些变化,这些变化是为了更好地支持Go的错误处理提案。Go 1.20中也增加了一个新方法,这个新方法可以代替第三方的库处理多个error,这篇文章将介绍这些变化。

因为原来的Go的errors中的内容非常的简单,可能会导致大家轻视这个包,对于新的变化不是那么的关注。让我们一一介绍这些新的方法。

Unwrap

如果一个err实现了Unwrap函数,那么errors.Unwrap会返回这个err的unwrap方法的结果,否则返回nil。
一般标准的error都没有实现Unwrap方法,比如io.EOF, 但是也有一小部分的error实现了Unwrap方法,比如os.PathErroros.LinkErroros.SyscallErrornet.OpErrornet.DNSConfigError等等。

比如下面的代码:

fmt.Println(errors.Unwrap(io.EOF)) // nil
_, err := net.Dial("tcp", "invalid.address:80")
fmt.Println(errors.Unwrap(err))

第一行因为io.EOF没有Unwrap方法,所以输出nil。
net.Dial失败返回的err是*net.OpError,它实现了Unwrap方法,返回更底层的*net.DNSError,所以第二行输出为lookup invalid.address: no such host

最常用的,我们使用fmt.Errorf + %w包装一个error,比如下面的代码:

e1 := fmt.Errorf("e1: %w", io.EOF)
e2 := fmt.Errorf("e2: %w + %w", e1, io.ErrClosedPipe)
e3 := fmt.Errorf("e3: %w", e2)
e4 := fmt.Errorf("e4: %w", e3)
fmt.Println(errors.Unwrap(e4)) // e3: e2: e1: EOF + io: read/write on closed pipe

这段代码逐层进行了包装,最后的e4包含了所有的error,我们可以通过errors.Unwrap逐层进行解包,直到最底层的error。
fmt.Errorf可以1一次包装多个error,比如上面的e2,它包含了e1io.ErrClosedPipe两个error。

我们常常在多层调用的时候,把最底层的error逐层包装传递上去,这个时候我们可以使用fmt.Errorf + %w包装error。
在最高层处理error的时候,再逐层Unwrap解开error,逐层处理。

Is函数检查error的树中是否包含指定的目标error。

啥是error的? 一个error的数包括它本身,以及通过Unwrap方法逐层解开的error。
error的Unwrap方法的返回值,可能是单个error,也可能是是多个error,在返回多个error的时候,会采用深度优先的方式进行遍历检查,寻找目标error。

怎么才算找到目标error呢?一种情况就是此err就是目标error,这没有什么好说的,第二种就是此err实现了Is(err)方法,把目标err扔进Is方法返回true。

所以从功能上看Is函数其实叫做Has函数更贴切些。

下面是一个例子:

e1 := fmt.Errorf("e1: %w", io.EOF)
e2 := fmt.Errorf("e2: %w + %w", e1, io.ErrClosedPipe)
e3 := fmt.Errorf("e3: %w", e2)
e4 := fmt.Errorf("e4: %w", e3)
fmt.Println(errors.Is(e4, io.EOF)) // true
fmt.Println(errors.Is(e4, io.ErrClosedPipe)) // true
fmt.Println(errors.Is(e4, io.ErrUnexpectedEOF)) // false

Is是遍历error的数,检查是否包含目标error。
As是遍历error的数,检查每一个error,看看是否可以把从error赋值给目标变量,如果是,则返回true,并且目标变量已赋值,否则返回false。

下面这个例子,我们可以看到As的用法:

if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("failed at path:", pathError.Path)
} else {
fmt.Println(err)

如果os.Open返回的error的树中包含*fs.PathError,那么errors.As会把这个error赋值给pathError变量,并且返回true,否则返回false。
我们这个例子正好制造的就是文件不存在的error,所以它会输出:failed at path: non-existing

经常常犯的一个错误就是我们使用一个error变量作为As的第二个参数。下面这个例子tmp就是error接口类型,所以origin可以直接赋值给tmp,所以errors.As返回true,并且tmp的值就是origin的值。

var origin = fmt.Errorf("error: %w", io.EOF)
var tmp = io.ErrClosedPipe
if errors.As(origin, &tmp) {
fmt.Println(tmp) // error: EOF

As使用起来总是那么别别扭扭,每次总得声明一个变量,然后把这个变量传递给As函数,在Go支持泛型之后,As应该可以简化成如下的方式:

func As[T error](err error) (T, bool)

但是,Go不会修改这个导致不兼容的API,所以我们只能继续保留As函数,增加一个新的函数是一个可行的方法,无论它叫做IsAAsOf还是AsTarget或者其他。

如果你已经掌握了Go的泛型,你可以自己实现一个As函数,比如下面的代码:

func AsA[T error](err error) (T, bool) {
var isErr T
if errors.As(err, &isErr) {
return isErr, true
var zero T
return zero, false

写段测试代码,我们可以看到它的效果:

type MyError struct{}
func (*MyError) Error() string { return "MyError" }
func main() {
var err error = fmt.Errorf("error: %w", &MyError{})
m, ok := AsA[*MyError](err) // MyError does not implement error (Error method has pointer receiver)
fmt.Println(m, ok)

大家在#51945讨论了一段时间,又是无疾而终了。

在我们的项目中,有时候需要处理多个error,比如下面的代码:

func (s *Server) Serve() error {
var errs []error
if err := s.init(); err != nil {
errs = append(errs, err)
if err := s.start(); err != nil {
errs = append(errs, err)
if err := s.stop(); err != nil {
errs = append(errs, err)
if len(errs) > 0 {
return fmt.Errorf("server error: %v", errs)
return nil

这段代码中,我们需要处理三个error,如果有一个error不为nil,那么我们就返回errs。
当然,为了处理多个errors情况,先前,有很多的第三方库可以供我们使用,比如

  • go.uber.org/multierr
  • github.com/hashicorp/go-multierror
  • github.com/cockroachdb/errors

但是现在,你不用再造轮子或者使用第三方库了,因为Go 1.20中增加了errors.Join函数,它可以把多个error合并成一个error,比如下面的代码:

var e1 = io.EOF
var e2 = io.ErrClosedPipe
var e3 = io.ErrNoProgress
var e4 = io.ErrShortBuffer
_, e5 := net.Dial("tcp", "invalid.address:80")
e6 := os.Remove("/path/to/nonexistent/file")
var e = errors.Join(e1, e2)
e = errors.Join(e, e3)
e = errors.Join(e, e4)
e = errors.Join(e, e5)
e = errors.Join(e, e6)
fmt.Println(e.Error())
// 输出如下,每一个err一行
// EOF
// io: read/write on closed pipe
// multiple Read calls return no data or error
// short buffer
// dial tcp: lookup invalid.address: no such host
// remove /path/to/nonexistent/file: no such file or directory
fmt.Println(errors.Unwrap(e)) // nil
fmt.Println(errors.Is(e, e6)) //true
fmt.Println(errors.Is(e, e3)) // true
fmt.Println(errors.Is(e, e1)) // true

你可以使用Is判断是否包含某个error,或者使用As提取出目标error。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK