Go语言之errors包源码分析

362 阅读4分钟

「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

errors包

在builtin包中指定了很多内置类型和一些内置函数,其中使用非常广的一个是error接口.

    type error interface {
      Error() string
    }

在errors包中有对error接口的实现.

    package errors

    func New(text string) error {
      return &errorString{text}
    }

    type errorString struct {
      s string
    }

    func (e *errorString) Error() string {
      return e.s
    }

整个errors包对error的实现非常简单,只是扩展了一个错误信息. 对外暴露的就是构造函数New().

扩展

Unwrap()扩展

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

如果一个错误(这里指实现了error接口的类型)同时还实现了interface{Unwrap()error}, 那么调用Unwarp()就可以获取到更深层次的错误.

这里有趣的是Unwrap()返回的是error类型,所以可以用于链式扩展. 调用一次Unwrap(),就是解开一层,直到最后一层原始error时,Unwrap返回nil.

Is()扩展

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

      isComparable := reflectlite.TypeOf(target).Comparable()
      for {
        if isComparable && err == target {
          return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
          return true
        }
        if err = Unwrap(err); err == nil {
          return false
        }
      }
    }

这个是比较两个error是否相等(这里的相等也是需要定义的), 要么是通过反射来进行比较,要么是实现 interface { Is(error) bool }来确定比较, 如果这两种方式都不行,就将第一个比较值(对应代码中的err)进行Unwrap, 这里比较适合链式错误的比较.

As()扩展,是一个赋值,也支持链式.

errors测试

  • TestNewEqual
    • 测试构造出的error是否相等
    • 不同的New是不同的
    • 同一个New出来的变量是可以和自己做比较的,而且应该相等
  • 其他单元测试

扩展测试

这里面的测试对象之一是wrapped结构体

    type wrapped struct {
      msg string
      err error
    }

    func (e wrapped) Error() string { return e.msg }

    func (e wrapped) Unwrap() error { return e.err }

实现了error接口和interface { Unwarp() error }接口.

另一个测试对象是poser结构体

    type poser struct {
      msg string
      f   func(error) bool
    }

    var poserPathErr = &os.PathError{Op: "poser"}

    func (p *poser) Error() string     { return p.msg }
    func (p *poser) Is(err error) bool { return p.f(err) }
    func (p *poser) As(err interface{}) bool {
      switch x := err.(type) {
      case **poser:
        *x = p
      case *errorT:
        *x = errorT{"poser"}
      case **os.PathError:
        *x = poserPathErr
      default:
        return false
      }
      return true
    }

poser实现了error接口,以及Is/As对应的接口.

先分析最基础的TestUnwrap().

    func TestUnwrap(t *testing.T) {
      err1 := errors.New("1")
      erra := wrapped{"wrap 2", err1}

      testCases := []struct {
        err  error
        want error
      }{
        {nil, nil},
        {wrapped{"wrapped", nil}, nil},
        {err1, nil},
        {erra, err1},
        {wrapped{"wrap 3", erra}, erra},
      }
      for _, tc := range testCases {
        if got := errors.Unwrap(tc.err); got != tc.want {
          t.Errorf("Unwrap(%v) = %v, want %v", tc.err, got, tc.want)
        }
      }
    }

erra对err1封装了一层,再利用表格测试来测试不同的场景. 具体的测试功能是解封一层,即对error调用Unwrap().

再看看TestIs().也是表格测试,写法上稍微有些区别, 就是这里的测试将每个测试项都作为一个测试点. 这里分3块看:

  • wrapped类的数据
    • 这类数据只包含错误链
    • 而且只那第一个参数的错误链和第二个参数做比较
  • poser类的数据
    • 这类数据实现了Is()方法集对应的接口
    • 测试函数中Is()的逻辑是判断err1/err3
    • 如果poser实现了Unwrap(),那结果又不一样了
  • errorUncomparable类的数据
    • 这个类型的Is,只判断参数是不是errorUncomparable值类型
    • 而且结构体里的数据是字符串切片,不可比较

接下来看TestAs().反射库用的较多,后面再分析.

总结

对于errors包:

  • errors.New构造的两个错误对象是不相等的,即使错误消息是一样的
  • errors.Is(err,targe),是拿err错误链和targe比较,targe是不解封的(层层剥开)

写法上:

  • 函数或结构体等引用的部分会在后面出现
    • c/c++中的函数必须前置申明,在Go并不要求如此
  • 对于简单的函数的单元测试,主要以表格测试为主(此处主要测试边界)
  • 对于待测试的包,相应的测试包名是在后面添加"_test"
    • eg: io_test errors_test
  • 测试包对于待测试包的引用,可以使用别名".",也可以不用
    • io包用".", erros包就没有用别名