Go学堂

V1

2022/10/20阅读:29主题:橙心

「Go工具箱」一个简单、易用的多错误管理包:go-multierror

白海豚.webp 大家好,我是渔夫子。本号新推出「Go 工具箱」系列,意在给大家分享使用 go 语言编写的、实用的、好玩的工具。

今天给大家推荐的是一个多错误管理包工具:go-multierror。

该包可以将多个错误合并成一个标准的 error,使得多个错误管理变得更容易。同时,该包和 go 标准库中的 error 包完全兼容,包括 As、Is 和 Unwrap 函数。

小档案

go-multierror 小档案
star 1.7k used by 38.6k
contributors 16 作者 HashiCorp(机构)
功能简介 多错误管理包。可以将多个错误合并成一个标准的 error,使得多个错误管理变得更容易。
项目地址 https://github.com/hashicorp/go-multierror[1]
相关知识 error 处理

一、安装

安装

使用 go get 进行安装

go get github.com/hashicorp/go-multierror

go 版本要求

该最新包需要依赖于 Go 的 1.13 或更高版本,因为 error 中的 wrap 功能是从 1.13 版本开始的。如果你当前的 go 版本低于 1.13,那么可以使用该包的 v1.0.0 tag,该版本不依赖于 1.13 中的 wrap 功能。

go get github.com/hashicorp/go-multierror@v1.0.0

知识点:在 Go 1.13 版本之前,标准库对 error 的支持仅有 errors.New()和 fmt.Errorf()两个函数来构造 error 实例。从 1.13 版本开始,在 errors 和 fmt 标准库包中引入了新功能以简化处理包含其他错误的错误,称之为链式 error。其中就包含 errors.Unwrap()、errors.Is()和 errors.As(),以及 fmt.Errorf 中引入了%w 动词以创建 wrapError。

二、基本使用

mutlierror 包的使用也非常简单。下面我们看下其主要的使用。

构建错误列表

通过 mutierror 包中的 Append 函数可以创建错误列表。该函数的行为非常类似 go 内建的 append 函数。Append 的第一个参数无论是 nil、multierror.Error 或者其他类型的 error,该函数都会返回一个 multierror.Error 类型的值,并将 Append 中第二个参数中的 err 加入到 multierror.Error 的列表中。

var result error

if err := step1(); err != nil {
 result = multierror.Append(result, err)
}
if err := step2(); err != nil {
 result = multierror.Append(result, err)
}

return result

自定义格式化输出

通过指定 multierror.Error 的实例变量中的 ErrorFormat 属性,就可以自定义 Error() string 的输出格式:

var result *multierror.Error

// ... accumulate errors here, maybe using Append

if result != nil {
 result.ErrorFormat = func([]error) string {
  return "errors!"
 }
}

访问错误列表

multierror.Error 实现了 error 接口,所以即使调用者不知道返回的错误类型是否是 multierror,该错误依然能正常工作。同时,我们也可以通过类型断言的方式来校验返回的错误是否是 multierror.Error 类型,以便可以访问 multierror.Error 中的所有 error。

if err := something(); err != nil {
 if merr, ok := err.(*multierror.Error); ok {
  // Use merr.Errors
 }
}

当然,也可以使用 go 内建包 errors.Unwrap()函数对 mutlierror.Errors 依次解析,直到所有的 error 都被解析完成。

知识点:内建包中的 errors.Unwrap()函数,可以对 error 层层拆解。对于自定义的 error 类型,不仅要实现 Error()函数,同时也需要实现 Unwrap 函数。

errors.Unwrap()函数的源代码如下:

func Unwrap(err error) error {
> u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

如果参数 err 没有实现 Unwrap()函数,则说明是基础 error,直接返回 nil,否则调用元 err 实现的 Unwrap()函数并返回。

提取特定的 error 值

标准库中的 errors.As 函数可以直接从 multierror.Error 中提取一个特定的 error:

// Assume err is a multierror value
err := somefunc()

// We want to know if "err" has a "RichErrorType" in it and extract it.
var errRich RichErrorType
if errors.As(err, &errRich) {
 // It has it, and now errRich is populated.
}

知识点:标准库中的 errors.As 函数会调用 Unwrap 函数,将 err 层层拆解,如果拆解到的 error 和目标 error 类型相同,则将该 error 赋值给目标参数 errRich。

检查 multierror.Error 中是否有具体的错误值

有时候一些函数会返回具体的错误值,比如 os 包中返回的 ErrNotExists。所以,我们就可以通过 errors.Is 函数来检查 multierror.Error 中是否存在具体的错误值。

// Assume err is a multierror value
err := somefunc()
if errors.Is(err, os.ErrNotExist) {
 // err contains os.ErrNotExist
}

知识点:errors.Is 用来判断链式 err 中是否有具体的 error 值(通常称之为哨兵 error)

错误处理

在实际使用中,错误都是由从函数中创建并返回的。调用者用 if 语句判断返回的错误是否为 nil(error 飞初始化的值)来判断错误是否存在。那么,在 multierror.Error 类型中,何时返回 nil,何时返回错误呢? 在 multierror.Error 的实例中,可以通过该类型的 ErrorOrNil 方法来返回错误或 nil。该函数内部实现中判断该实例中的 Errors 切片是否为空,如果不为空,则返回该实例,否则返回 nil。

var result *multierror.Error

// ... accumulate errors here

// Return the `error` only if errors were added to the multierror, otherwise
// return nil since there are no errors.
return result.ErrorOrNil()

ErrorOrNil 函数的实现如下:

func (e *Error) ErrorOrNil() error {
 if e == nil {
  return nil
 }
 if len(e.Errors) == 0 {
  return nil
 }

 return e
}

知识点:在 Go 中错误一般是从函数中创建并作为值返回的。调用者需要使用 if 语句判断返回的错误是否为 nil 来判断错误是否存在。 同时,在 Go 中,函数有多个返回值时,错误一般放到最后。

三、实现原理分析

multierror.Error 类型的定义

multierror.Error 类型的结构很简单,因为要实现多错误管理,所以有一个 error 类型的切片;另外还有一个 ErrorformatFunc 函数类型,用于格式化输出 Error 的描述。如下:

type Error struct {
 Errors      []error
 ErrorFormat ErrorFormatFunc
}

type ErrorFormatFunc func([]error) string

这里我们需要提及到 golang 中的 error 类型的知识点。

知识点:Golang 中的 error 实质上就是一个简单的接口类型。只要实现了这个接口,就可以将其视为一种 error。

type error interface {
    Error() string
}

所以,multierror.Error 类型也实现了 Error()方法:


func (e *Error) Error() string {
 fn := e.ErrorFormat
 if fn == nil {
  fn = ListFormatFunc
 }

 return fn(e.Errors)
}

multierror.Error 类型实现了 error 接口,那么该类型的变量就可以存储到 error 接口的变量中。

知识点:任何类型只要实现了 interface 类型的所有方法,就可以声称该类型实现了这个接口,该类型的变量就可以存储到 interface 变量中。

multierror.Append 函数的实现

在基本使用一节我们提到,可以通过 Append 函数来构建一个具体的多错误值实例 multierror.Error。这个本质上是将 error 值键入到 multierror.Error 类型的 Errors 切片中。

同时,我们提到,该 Append 函数无论是我们看下是如何实现:

  • 首先通过类型断言 err.(type)来判断 err 的类型
  • 如果参数中的 err 不是 multierror.Error 类型,则新构建一个 errors 切片,将 err 和 errs 都加入到切片中,然后再构建一个空 multierrors.Error 类型的实例,然后再递归调用 multierrors.Append 函数。
  • 如果参数中的 err 的类型就是 Error,那么就将 errs 错误加入到 Error 结构类型中的 Errors 切片中。

具体实现如下:

func Append(err error, errs ...error) *Error {
 switch err := err.(type) {
 case *Error:
  // Typed nils can reach here, so initialize if we are nil
  if err == nil {
   err = new(Error)
  }

  // Go through each error and flatten
  for _, e := range errs {
   switch e := e.(type) {
   case *Error:
    if e != nil {
     err.Errors = append(err.Errors, e.Errors...)
    }
   default:
    if e != nil {
     err.Errors = append(err.Errors, e)
    }
   }
  }

  return err
 default:
  newErrs := make([]error, 0len(errs)+1)
  if err != nil {
   newErrs = append(newErrs, err)
  }
  newErrs = append(newErrs, errs...)

  return Append(&Error{}, newErrs...)
 }
}

自定义格式化错误输出实现

在基本使用中提到,可以给 multierrors.Error 类型中的 ErrorFormat 属性赋值一个输出错误的函数,这样就能按自定义函数的格式将错误列表输出了。该输出的实现实际是在 Error 函数中定义的:

func (e *Error) Error() string {
 fn := e.ErrorFormat
 if fn == nil {
  fn = ListFormatFunc
 }

 return fn(e.Errors)
}


var result *multierror.Error
if result != nil {
 result.ErrorFormat = func([]error) string {
  return "errors!"
 }
}

result.Error() //就会按照ErrorFormat函数输出错误

应用场景

多错误管理的应用场景一般是用在一个函数的逻辑中需要把所有的错误都返回的情况。比如在服务启动时,对 redis、kafka、mysql 等各种资源初始化场景,可以把所有相关资源初始化的错误都返回。还有一种场景就是在 web 请求中,校验请求参数时,返回所有参数的校验错误给客户端的场景。

---特别推荐---

特别推荐:一个专注go项目实战、项目中踩坑经验及避坑指南、各种好玩的go工具的公众号。「Go学堂」,专注实用性,非常值得大家关注。点击下方公众号卡片,直接关注。关注送《100个go常见的错误》pdf文档。

参考资料

[1]

https://github.com/hashicorp/go-multierror: https://github.com/hashicorp/go-multierror

分类:

后端

标签:

Golang

作者介绍

Go学堂
V1