Golang中的错误处理

Golang 错误处理

go的错误处理与其他一些主流编程语言具有本质的区别,go的内嵌类型error不含有stack trace,也不支持传统的 try/catch 语句模型。go中的错误只是函数返回的一个正常的变量值,对待错误也能像对待普通变量一样,go的这种错误模型带来了更轻易、更简便的程序设计风格。

error interface

builtin.go中包含下述的一个接口类型:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

go中的 error 接口只有一个方法 Error 返回一个字符串,该字符串就可以包含该错误的内部信息,没有任何其他方法,错误就是这么简单!

构造 error

构造 error 一般有两种方法:errors.New("")fmt.Errorf(""),区别是:

  1. errors.New的参数只能传入字符串常量

  2. fmt.Errorf可以利用format的方式构造字符串

自定义error

自定义 error 只要实现 builtin.go 中的 error 接口就可以向上兼容 error。同时我们可以利用 go 嵌套接口的方法去定义一个我们自己的 error 接口:

type myErr interface {
    error
    Desc()   string
    String() string
}

再定义结构体去实现该接口,我们就有了自己定义的 custom error了。

wrap 和 unwrap

有一定的 golang 编程经历的同学会发现,error 没有stack trace的特性会导致程序中的错误比较难以跟踪。为了解决这种问题就出现了 error wraping 的一种操作。如果你查看 fmt.Errorf 的源代码,你会明显的发现 error wraping 最简单的原理:

func Errorf(format string, a ...any) error {
	p := newPrinter()
	p.wrapErrs = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	if p.wrappedErr == nil {
		err = errors.New(s)
	} else {
		err = &wrapError{s, p.wrappedErr}
	}
	p.free()
	return err
}

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

首先注意结构体 wrapError 其内部包含了一个 msg 和另一个 error 对象,也就是说 wrapError 本身是一个 error 的实现体,另外它内部又包含了一个 error 接口对象,这就看起来很像是设计模式中的 Composite Patternwrap 操作其实就是将给定的 error 打包到内部,unwrap 操作就是将内部的 error 返回出去。

我们可以在 Errorf 函数的 format 中添加 %w 参数代表将此 error wrap 到新的 error 内部。

user, err := findUserById(id)
if err != nil {
  return nil, fmt.Errorf("%w find user error:%s",err, err.Error())
}

有了 wrapunwrap 操作后,我们返回的每一个错误都可以包含一个内部错误,这样我们就可以沿着内部错误找到错误的根源。

errors 包

我们重点讨论 errors/wrap.go 中的内容,此文件下只包含三个函数,分别是 UnwrapIsAs

Unwrap

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

容易看出,Unwrap只是判断了当前传入的 err 是否实现了 Unwrap 方法,若是调用其 Unwrap 方法并返回,否则返回 nil。

这个函数存在的意义在于以下:

  • 在开发当中,我们一般都会使用builtin.go中的 error 接口作为我们的错误返回值,但是 error 接口并没有定义 Unwrap 方法,所以我们需要一种方法去先判断是否能进行Unwrap,如果可以,转化并Unwrap,如果不行,返回nil。这就是 errors.Unwrap 出现的核心作用。(至少我是这么理解的,若有误,请纠正)

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
      }
   }
}

Is 的核心作用是为了判断传入的第一个error(err)是否 包含 第二个error(target)。判断逻辑是一种 循环 + 递归(说是递归也不太正确,内部调用的方法虽然是同名的,但不是同一个函数) 的方式:

  1. 若 target 为空,返回 err 是否为空,否则到 2;
  2. 获取 target 类型,并判断其是否是可比较类型 (isComparable);
  3. 若 target 可比较且 err == target,返回true;否则到4;
  4. 判断 err 是否实现了 Is(error) bool 的方法,若是调用该方法,true就返回true,false 或 没有实现该方法都到5;
  5. 对 err 进行 Unwrap 操作获取其内部的 error,若获取到空,返回false,否则返回3;

这里的逻辑就是,err 若实现了 Is 就调用,没有就继续往下拆(获取内部的wrapped error),只要拆出来有一个 err == target 或 err 实现了 Is 并在调用后返回 true 时,整个函数就返回 true,其他情况返回 false。

As

func As(err error, target any) bool {
	if target == nil {
		panic("errors: target cannot be nil")
	}
	val := reflectlite.ValueOf(target)
	typ := val.Type()
	if typ.Kind() != reflectlite.Ptr || val.IsNil() {
		panic("errors: target must be a non-nil pointer")
	}
	targetType := typ.Elem()
	if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	for err != nil {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
			return true
		}
		err = Unwrap(err)
	}
	return false
}

注释中是这么描述 As 方法的:As finds the first error in err's chain that matches target, and if one is found, sets target to that error value and returns true. Otherwise, it returns false.

也就是说,As 从传入的 error 开始遍历错误链(由Unwrap操作获取的error组成的链),寻找可为 target 赋值的error,找到就赋值并返回true,否则返回false。除了这些,函数对target的类型也有一些约束:如果是指针不能是nil,如果是结构体必须实现 error 接口等。

关于 golang 中的错误处理的知识绝对不止这些,希望大家能提出更多的想法来补充我的文档!

你可能感兴趣的:(golang,开发语言,后端)