错误处理是任何编程语言都绕不开的话题。始终以来,编程语言的错误处理机制有两大流派:基于异样的结构化 try-catch-finally 解决机制和基于值的解决机制。前者的成员包含 C++、Java、Python、PHP 等支流编程语言,后者的代表则是 C 语言。Go 的设计谋求简略,采纳的是后一种解决机制:谬误就是值,而错误处理就是基于值比拟后的决策。

意识 error

在 Go 语言中,谬误是值,不过是一个接口值,也即咱们平时罕用的 error:

// $GOROOT/src/builtin/builtin.gotype interface error {    Error() string}                        

error 接口很简略,只申明了一个 Error() 办法。在规范库中提供了结构谬误值的两种根本办法:errors.New() 和 fmt.Errorf(),在 Go 1.13 版本之前,这两种办法实际上返回的是一个未导出类型 errors.errorString:

// $GOROOT/src/errors/errors.gofunc New(text string) error {    return &errorString{text}}type errorString struct {    s string}func (e *errorString) Error() string {    return e.s}// $GOROOT/src/fmt/errors.go// 1.13 版本之前func Errorf(format string, a ...interface{}) error {    return errors.New(Sprintf(format, a...))}                       

fmt.Errorf() 实用于须要格式化输入字符串的场景,如果不须要格式化字符串,则倡议应用 errors.New()。因为 fmt.Errof() 在生成格式化字符串时须要遍历所有字符,会有肯定的性能损失。

错误处理根本策略

理解了谬误值后,咱们来看一下 Go 语言错误处理的几种习用策略。

通明策略

通明解决策略是最简略的策略,它齐全不关怀返回谬误值携带的具体上下文信息,只有产生谬误就进入惟一的错误处理执行门路。这也是 Go 语言中最常见的错误处理策略,绝大部分的错误处理情景能够归类到这种策略下。

err := doSomething()if err != nil {    // 不关怀err变量底层谬误值所携带的具体上下文信息    // 执行简略错误处理逻辑并返回    ...    return err}                        

“哨兵”解决策略

“哨兵”策略通过特定值来示意胜利和不同的谬误,依附调用方对谬误进行查看来处理错误。如果采纳这种解决策略,谬误值结构方通常会定义一系列导出的“哨兵”谬误值,用来辅助错误处理方检视谬误值并做出错误处理分支的决策。

// $GOROOT/src/bufio/bufio.govar (    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")    ErrBufferFull        = errors.New("bufio: buffer full")    ErrNegativeCount     = errors.New("bufio: negative count"))// 错误处理代码data, err := b.Peek(1)if err != nil {    switch err {    case bufio.ErrNegativeCount:        // ...        return    case bufio.ErrBufferFull:        // ...        return    case bufio.ErrInvalidUnreadByte:        // ...        return    default:        // ...        return    }}            

与通明谬误策略相比,“哨兵”策略让错误处理方能够更灵便地处理错误。不过对于包的开发者而言,裸露“哨兵”谬误值意味着这些谬误值和包的公共函数一起成为包的一部分,会让错误处理方对其产生依赖。

类型检视策略

类型检视策略又被称为自定义谬误策略,顾名思义,这种错误处理形式通过自定义的谬误类型来示意特定的谬误,同样依赖下层代码对谬误值进行查看,不同的是须要应用类型断言机制(type assertion)或类型抉择机制(type switch)对谬误进行查看。

来看一个规范库的例子:

// $GOROOT/src/encoding/json/decode.gotype UnmarshalTypeError struct {    Value  string           Type   reflect.Type     Offset int64            Struct string          Field  string       }            // $GOROOT/src/encoding/json/decode_test.go// 通过类型断言机制获取func TestUnmarshalTypeError(t *testing.T) {    for _, item := range decodeTypeErrorTests {        err := Unmarshal([]byte(item.src), item.dest)        if _, ok := err.(*UnmarshalTypeError); !ok {            t.Errorf("expected type error for Unmarshal(%q, type %T): got %T",                    item.src, item.dest, err)        }    }}// $GOROOT/src/encoding/json/decode.go// 通过类型抉择机制获取func (d *decodeState) addErrorContext(err error) error {    if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 {        switch err := err.(type) {        case *UnmarshalTypeError:            err.Struct = d.errorContext.Struct.Name()            err.Field = strings.Join(d.errorContext.FieldStack, ".")            return err        }    }    return err}                                    

这种错误处理的益处在于能够将谬误包装起来,提供更多的上下文信息,但实现方必须向下层公开实现的谬误类型,与应用方之间同样须要产生依赖关系。

链式 error

在 1.13 版本之前,应用上述“哨兵”和类型检视策略带来的最大的一个问题是 error 通过函数或办法进行自定义解决后,原始的 error 会被抛弃。

// example 1func main() {    err := WriteFile("")    if err == os.ErrPermission {        fmt.Println("permission denied")    }}func WriteFile(filename string) error {    if filename == "" {        return fmt.Errorf("write file error: %v", os.ErrPermission)    }    return nil}// example 2func main() {    err := WriteFile("")    if _, ok := err.(*os.PathError); ok {        fmt.Println("permission denied")    }}func WriteFile(filename string) error {    if filename == "" {        return fmt.Errorf("write file error: %v", &os.PathError{})    }    return nil}

通过下面两个示例咱们能够看到,原始的 error 通过函数的自定义包装后,它的值或者类型就可能被“吞没”了,应用方不能很容易地获取到它,给错误处理带来了不必要的麻烦。

为了解决这个问题,Go 1.13 版本中引入了一套称为链式 error 的解决方案,error 在函数间传递时信息并不会失落,而是像链条一样被串连起来。

wrapError

wrapError 是链式 error 的外围数据结构,其余相干优化都是围绕它开展的:

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

wrapError 与传统的 errorString 相比,额定实现了 Unwrap 办法,用于返回原始 error。

生成链式 error

在 Go 1.13 版本之后,咱们能够应用 fmt.Errorf 函数配合格局动词 %w 来生成链式 error,源码如下:

func Errorf(format string, a ...interface{}) error {    p := newPrinter()    p.wrapErrs = true    // 解析格局,如果发现格局动词 %w 且提供了非法的 error 参数,则把 p.wrappedErr 置为 error     p.doPrintf(format, a)    s := string(p.buf)    var err error    if p.wrappedErr == nil {        // 个别状况下生成 errorString        err = errors.New(s)    } else {        // 存在 %w 动词生成 wrapError        err = &wrapError{s, p.wrappedErr}    }    p.free()    return err}

生成 wrapError 有两点须要记住:

  • 每次只能应用一次 %w 动词;
  • %w 动词只能匹配实现 error 接口的参数。

errors.Is

errors 包提供了 Is 办法用于错误处理方对谬误值进行比拟,Is 反对谬误在包装过多层后的等值判断。

func Is(err, target error) bool {    if target == nil {        return err == target    }    isComparable := reflectlite.TypeOf(target).Comparable()    for {        // 如果 target 是可比拟的,则间接进行比拟        if isComparable && err == target {            return true        }        // 如果 err 实现了 Is 办法,则调用该办法持续进行判断        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {            return true        }        // 否则,对 err 进行 Unwrap(也即返回 wrapError 的 err 字段)        if err = Unwrap(err); err == nil {            return false        }    }}

errors.As

As 办法相似于通过类型断言判断一个 error 类型变量是否为特定的自定义谬误类型。不同的是,如果 error 类型变量的底层谬误值是一个链式 error,那么 As 办法会沿着谬误链进行类型比拟,直至找到一个匹配的谬误类型。

func As(err error, target interface{}) bool {    if target == nil {        panic("errors: target cannot be nil")    }    // 通过反射获取 target 的值和类型,并进行相干判断    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 {        // 如果 err 的类型与 target 匹配,间接赋值给 target        if reflectlite.TypeOf(err).AssignableTo(targetType) {            val.Elem().Set(reflectlite.ValueOf(err))            return true        }        // 判断 err 是否实现 As 办法,若已实现则调用该办法进一步匹配        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {            return true        }        // 否则,对 err 进行 Unwrap        err = Unwrap(err)    }    return false}

错误处理倡议

对于错误处理的探讨有很多,但没有哪一种错误处理策略实用于所有我的项目或场合。综合上述的结构谬误值办法及错误处理策略,有以下几点倡议:

  • 优先应用通明错误处理策略,升高错误处理方与谬误值结构方之间的耦合;
  • 其次尽量应用类型检视策略;
  • 在上述两种策略无奈施行的状况下,再用“哨兵”策略;
  • 在 Go 1.13 及后续版本中,尽量用 errors.Is 和 errors.As 办法替换原先的错误处理语句。

优化 if err != nil

因为 Go 语言的错误处理机制,会在代码中产生大量的 if err != nil,非常繁琐且不美观,这也是 Go 语言常常被其余支流语言开发者吐槽的中央。那么有什么方法能够优化?

首先能想到的就是视觉上的优化,将多个判断语句搁置在一起,但这种办法也只不过是“外表功夫”,而且有很大的局限性。

第二种就是模拟其余语言用 panic 和 recover 来模仿异样捕捉来替换谬误值判断,不过这是一种反模式,并不举荐应用。首先,谬误是失常的编程逻辑,而异样是意料之外的谬误,二者不能画等号,而且如果异样没有失去捕捉将会导致整个过程退出,个别情况下结果很重大。还有一点,应用异样代替谬误机制会大幅影响程序的运行速度。

在这里提供两种优化思路以供参考。

封装多个 error

这个办法就是将多个 if err != nil 语句封装到一个函数或办法中,这样内部调用的时候只须要额定判断一次就能够了。上面看一个例子:

func openBoth(src, dst string) (*os.File, *os.File, error) {    var r, w *os.File    var err error    if r, err = os.Open(src); err != nil {        return nil, nil, fmt.Errorf("copy %s %s: %v", src, dst, err)    }        if w, err = os.Create(dst); err != nil {        r.Close()        return nil, nil, fmt.Errorf("copy %s %s: %v", src, dst, err)    }    return r, w, nil}func CopyFile(src, dst string) error {    var err error    var r, w *os.File    if r, w, err = openBoth(src, dst); err != nil {        return err    }    defer func() {        r.Close()        w.Close()        if err != nil {            os.Remove(dst)        }    }()        if _, err = io.Copy(w, r); err != nil {        return fmt.Errorf("copy %s %s: %v", src, dst, err)    }    return nil}                        

为了缩小 CopyFile 函数中 if err != nil 的反复次数,以上代码引入了一个 openBoth 函数,咱们将关上源文件、创立目标文件和相干的错误处理工作转移到了 openBoth 函数中。这种办法的长处是比较简单,毛病是成果有时并不显著。

内置 error

咱们先粗略看一下 bufio 包的 Writer 实现:

// $GOROOT/src/bufio/bufio.gotype Writer struct {    err error    buf []byte    n   int    wr  io.Writer}func (b *Writer) WriteByte(c byte) error {    if b.err != nil {        return b.err    }    if b.Available() <= 0 && b.Flush() != nil {        return b.err    }    b.buf[b.n] = c    b.n++    return nil}                       

能够看到,Writer 定义了一个 err 字段作为外部谬误状态值,它与 Writer 的实例绑定在了一起,并且在 WriteByte 办法的入口判断是否为 nil。一旦不为 nil,WriteByte 就间接返回内置的 err。咱们来应用这种思路来重构一下后面例子中的代码:

type FileCopier struct {    w   *os.File    r   *os.File    err error}func (f *FileCopier) open(path string) (*os.File, error) {    if f.err != nil {        return nil, f.err    }        h, err := os.Open(path)    if err != nil {        f.err = err        return nil, err    }    return h, nil}func (f *FileCopier) openSrc(path string) {    if f.err != nil {        return    }        f.r, f.err = f.open(path)    return}func (f *FileCopier) createDst(path string) {    if f.err != nil {        return    }        f.w, f.err = os.Create(path)    return}func (f *FileCopier) copy() {    if f.err != nil {        return    }        if _, err := io.Copy(f.w, f.r); err != nil {        f.err = err    }}func (f *FileCopier) CopyFile(src, dst string) error {    if f.err != nil {        return f.err    }        defer func() {        if f.r != nil {            f.r.Close()        }        if f.w != nil {            f.w.Close()        }        if f.err != nil {            if f.w != nil {                os.Remove(dst)            }        }    }()        f.openSrc(src)    f.createDst(dst)    f.copy()    return f.err}func main() {    var fc FileCopier    err := fc.CopyFile("foo.txt", "bar.txt")    if err != nil {        fmt.Println("copy file error:", err)        return    }    fmt.Println("copy file ok")}                        

咱们将原 CopyFile 函数彻底摈弃,将其逻辑封装到 FileCopier 构造的 CopyFile 办法中。FileCopier 构造内置了一个 err 字段用于保留外部的谬误状态,这样在 CopyFile 办法中,咱们只需依照失常业务逻辑,程序执行 openSrc、createDst 和 copy 即可,失常业务逻辑的视觉连续性就这样被很好地实现了。