Go语言error四问

什么是error

Go语言没有像Java/Python一样提供try&catch这种错误捕获方式,而是要求编码者显示地处理下游的传递的错误、显示地向上游抛出错误,也难怪总是有人吐槽Go在这方面的啰嗦。

1
2
3
4
5
6
7
8
9
10
11
var file *os.File
var err error
file, err = os.Open("something.txt")
if err != nil {
// 处理打开文件时的错误,比如panic
panic(file)
}
// 处理文件
fmt.Println(file.Stat())
// 处理关闭文件时的错误,比如直接忽略
_ = file.Close()

words.png

Go在标准库里提供了一种错误表现形式:error接口,定义很简单:

1
2
3
4
5
6
7
package builtin

// 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() string方法的类型都可以视作error,这就带来了极大的灵活性。

1
2
3
4
5
6
7
8
9
10
11
type Error1 string

func (e Error1) Error() string { return string(e) }

type Error2 struct{ msg string }

func (e *Error2) SetMsg(msg string) { e.msg = msg }
func (e *Error2) Error() string { return e.msg }

var err1 error = Error1("hello") // Error1可以被视为Error
var err2 error = &Error2{msg: "world"} // Error2也可以被视为Error

如何创建error

Go原生提供了两种方式来创建一个error

1
2
var err1 error = errors.New("error msg")
var err2 error = fmt.Errorf("error msg: %s", "hello")

前者比较简单,单纯地创建了一个string的封装类型:

1
2
3
4
5
6
7
8
9
10
package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error { return &errorString{text} }

// errorString is a trivial implementation of error.
type errorString struct{ s string }

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

后者在示例代码中也很简单,可以看成fmt.Sprintf+errors.New。当然Go1.13做了点小改进,下文会提到。

1
2
3
4
5
6
7
8
// Go1.12源码
package fmt

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}

但这两种方式还是太简陋了,毕竟携带的只是字符串信息。所以在实际应用场景中,更常见的做法是自定义错误类型:

1
2
3
4
5
6
7
8
type BizError struct { // 自定义业务错误
Code int32 // 错误码
Msg string // 错误提示
}

func (e *BizError) Error() string { return fmt.Sprintf("BizError<%d>: %s", e.Code, e.Msg) }

func NewBizErr(code int32, msg string) error { return &BizError{Code: code, Msg: msg} }

当然,为某些特定场景设计的公用错误(如标准库里的io.EOF)也会使用errors.New,这些错误的意思也足以用简短的字符串描述。

如何向上游传递error

由于Go中的错误一般是通过error接口以返回值的形式传递给上游的,且一般不会包含函数调用栈信息,所以在给上游传递error时就需要考虑附加上下文信息以便排查问题,特别是将下游的error传递给上游时。

1
2
3
4
5
6
7
8
9
10
11
12
13
func dial80() error {
addr, timeout := "127.0.0.1:80", time.Second
// do something
if ... {
timeout = time.Second * 3
}
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return err
}
_ = conn.Close()
return nil
}

比如这个简单的函数,如果上游有必要感知出现错误时的上下文信息(比如超时配置),应该怎么处理呢?

一种简单的方法是将错误信息重新拼装一下,比如:

1
2
3
4
5
6
7
8
func dial80() error {
...
if err != nil {
// 对error来说,%s会调用.Error()方法
return fmt.Errorf("dial80 timeout %s: %s", timeout, err)
}
...
}

另一种方法是将error再包装一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type dial80Err struct {
timeout time.Duration
err error
}

func (e *dial80Err) Error() string { return fmt.Sprintf("dial80 timeout %s: %s", e.timeout, e.err) }

func dial80() error {
...
if err != nil {
return &dial80Err{timeout: timeout, err: err}
}
...
}

但两种方法都并不完美。前者会丢失error中除.Error()返回值外的其它信息,后者用起来很繁琐,有没有更好的方法呢?当然是有的。Go1.13引入了Error wrapping的概念,通过扩展fmt.Errorf来包装error

1
2
3
4
5
6
7
func dial80() error {
...
if err != nil {
return fmt.Errorf("dial80 timeout %s: %w", timeout, err)
}
...
}

注意其中%w占位符,fmt.Errorf会特殊处理这个占位符,如果存在这个占位符,fmt.Errorf会将其对应的error与错误信息一起放入一个新的结构体里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Go1.13源码
package fmt

func Errorf(format string, a ...interface{}) error {
...
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
...
}

type wrapError struct {
msg string
err error
}

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

这种方法综合了前两种的优点,即使用便捷又不会丢失信息。所以在Go1.13之后,非常推荐使用%w占位符来为错误附加上下文信息并传递给上游。

如何分析下游传递的error

当我们得到下游传递的error且需要根据其类型、属性做进一步操作时,该怎么办呢?(当然,前提是下游不能使用errors.Newfmt.Errorf+%s来传递错误,笑)

由于下游可能传递wrapError给上游,所以上游并不能直接对error进行比较运算(==)、类型断言,而是要用errors包提供的API进行分析。

比如,用errors.Is替代==运算符来判断两个error是否相等:

1
2
3
4
5
6
7
8
9
10
11
myRead := func() ([]byte, error) {
...
if ... {
return nil, fmt.Errorf("myRead: %w", io.EOF)
}
...
}
buf, err := myRead()
if errors.Is(err, io.EOF) {
// do something
}

又比如,用errors.As替代.(type)来做类型断言:

1
2
3
4
5
6
7
8
9
err := dial80()
if err != nil {
var opErr *net.OpError
// 判断err是否为超时错误
if ok := errors.As(err, &opErr); ok && opErr.Timeout() {
// do something
}
// do something
}

github.com/pkg/errors

标准库中errors.Iserrors.As两个函数需要频繁使用性能较差的反射,errors.As使用起来也很繁琐,所以如果有条件的话可以使用功能更丰富的github.com/pkg/errors包,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// import github.com/pkg/errors
myRead := func() ([]byte, error) {
...
if ... {
// 效果类似 fmt.Errorf("myRead: %w", io.EOF)
return nil, errors.WithMessage(io.EOF, "myRead")
}
...
}
buf, err := myRead()
if errors.Is(err, io.EOF) {
// do something
}
// 谨慎使用
if errors.Cause(err) == io.EOF {
// do something
}

使用时在下游通过errors.WithMessage/errors.WithMessagef包装错误,在上游通过errors.Cause获取被包装的原始错误。不过errors.Cause不能穿透被fmt.Errorf+%w包装的wrapError,所以使用时一定要谨慎。

总结

  1. Go中的error是一个只需实现Error() string方法的接口。
  2. 除非是公用错误,否则尽量使用自定义错误类型来生成错误,而不是使用errors.Newfmt.Errorf
  3. 使用fmt.Errorf%w来为错误附加上下文信息。
  4. 使用errors.Iserrors.As来分析错误。
  5. 如有条件,可使用更优秀的github.com/pkg/errors包替代标准库的errors包和fmt.Errorf

Go语言error四问
https://www.yooo.ltd/2021/07/17/Go语言error四问/
作者
OrangeWolf
发布于
2021年7月17日
许可协议