go1.13之Error Warp


本文挑重点来看go1.13版本中对于错误处理部分提供的新功能(Error wrapping)(proposal参考资料1)。

提案中对于error新功能主要分两点:

  1. error可以包裹着其他error。而不是以前的做法,以字符串拼接方式往上传递。
  2. 使用%+v打印error时,带有堆栈信息,精确到函数名与行号。

其实这两块功能,很早前Dave Cheney就在一篇博文中论述过他对error处理的理解,并给出相关的开源包github.com/pkg/errors(这个库现在也处于维护状态,不再接受新功能)。

用过的同学应该比较熟悉,它解决的问题也是上面提到两点error新功能。

功能1

go1.13之前,方法内部逻辑中,遇到其它方法返回error时,一种粗暴的处理方式,直接return fmt.Errorf("operate failed %v", err)。这种做法问题是把err转换成为另一个字符串,原始的err被抹掉。

如果想添加额外的错误信息,又不想抹掉原始的err,可以封装一个struct,上层通过err.Err.(type)的方式来检查,但显然加大编码复杂度。

而用了go1.13之后,解决这个问题的方案就变成下面这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func foo() error {
  err := openSomething()
  // %v 变成了 %w
  return fmt.Errorf("operate failed %w", err)
}

func main() {
  err := foo()
  if errors.Is(err, *os.PathError) {
    var pe os.PathError
    errors.As(err, &pe)
    // dosomething
  }

  // 或者更直接
  // var pe os.PathError
  // if errors.As(err, &pe) {
      // dosomething
  // }
}

这样原始的err不会变抹掉,通过errors.Is()方法可以检查出来。其次通过fmt.Println()输出是仍是字符串,样式与之前使用%v`时相比较没有改变。

还可以通过errors.As()方法将对应的原始err提取出来。

所以用户在升级新版本后,有两个地方的代码需要转换下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// before
if err == io.ErrUnexpectedEOF
// after
if errors.Is(err, io.ErrUnexpectedEOF)

// before
if e, ok := err.(*os.PathError); ok
// after
var e *os.PathError
if errors.As(err, &e)

用了这两种做法,即使API提供方后面更改返回的error含义,兼容成本较低。

基于go1.13之前不同error不同的使用场景,官方写了FAQ,参考资料3。

功能2

打印error时带上堆栈信息功能很实用,之前遇到error时,排查都需要顺藤摸瓜的找到源头,比较浪费时间。

坏消息,功能2在go1.13官方标准库中被腰斩了,推迟到go1.14。详细原因可参考资料2。

好消息是官方提供了golang.org/x/xerrors。这个包完整实现了这两个新功能点,主要生产环境不易升级go版本的用户,可以尝鲜,或者是对已经升级为新版本的下游做兼容。

看一下使用xerrors包打印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
var myerror = xerrors.New("myerror")
func foo() error {
	return myerror
}
func foo1() error {
	return xerrors.Errorf("foo1 : %w",foo())
}
func foo2() error {
	return xerrors.Errorf("foo2 : %w",foo1())
}
func main() {
	err := foo2()
	fmt.Printf("%v\n", err)
	fmt.Printf("%+v\n", err)
}

// 以下是输出
foo2 : foo1 : myerror
foo2 :
    main.foo2
        /Users/cbsheng/goproject/src/test/main.go:116
  - foo1 :
    main.foo1
        /Users/cbsheng/goproject/src/test/main.go:113
  - myerror:
    main.init
        /Users/cbsheng/goproject/src/test/main.go:108

注意,xerrors.Errorf("foo1 : %w",foo())中必须以: %w的格式占位,否则不起作用。

资料1:https://go.googlesource.com/proposal/+/master/design/29934-error-values.md

资料2:https://github.com/golang/go/issues/29934#issuecomment-489682919

资料3:https://github.com/golang/go/wiki/ErrorValueFAQ