IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    smallnest发表于 2023-12-13 15:24:34
    love 0

    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.PathError,os.LinkError、os.SyscallError、net.OpError、net.DNSConfigError等等。

    比如下面的代码:

    1
    2
    3
    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,比如下面的代码:

    1
    2
    3
    4
    5
    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,它包含了e1和io.ErrClosedPipe两个error。

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

    Is

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

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

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

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

    下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    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

    As

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    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的值。

    1
    2
    3
    4
    5
    6
    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应该可以简化成如下的方式:

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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讨论了一段时间,又是无疾而终了。

    Join

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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,比如下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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。



沪ICP备19023445号-2号
友情链接