Go 进阶训练营 – 错误处理二:错误定义与处理
本文最后更新于 600 天前,其中的信息可能已经有所发展或是发生改变。

error type: 错误定义与判断

Sentinel Error

哨兵错误,就是定义一些包级别的错误变量,然后在调用的时候外部包可以直接对比变量进行判定,在标准库当中大量的使用了这种方式。例如下方 io 库中定义的错误。

// EOF is the error returned by Read when no more input is available.
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")

// ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = errors.New("unexpected EOF")

// ErrNoProgress is returned by some clients of an io.Reader when
// many calls to Read have failed to return any data or error,
// usually the sign of a broken io.Reader implementation.
var ErrNoProgress = errors.New("multiple Read calls return no data or error")
  • 并不是所有error都表示错误,例如:io.EOF,可以理解为一个标识,代表数据读取完毕。

我们在外部判定的时候一般使用等值判定或者使用 errors.Is 进行判断。

if err == io.EOF {
    //...
}

if errors.Is(err, io.EOF){
    //...
}
  • 会导致包与包之间存在依赖,阻碍重构,并且携带信息有限。
  • 等值判断处理不了errwrap的情况,只能通过error.Error 的输出内容进行匹配。但是错误处理不能依赖 error.Error 的输出内容,这是给人看的,不是给程序看的。
  • 应比较原始错误,可通过errors.Cause()获取。
  • 有人指出Sentinel Error可以在test中使用,但是Dave认为这是一种code smell,应该避免(可能会泛滥)。

结论

不建议使用,或者至少不能用于公共API。

error types

这个就类似我们前面定义的 errorString 一样实现了 error 的接口,然后在外部是否类型断言来判断是否是这种错误类型

type MyStruct struct {
    s string
    name string
    path string
}

// 使用的时候
func f() {
    switch err.(type) {
        case *MyStruct:
        // ...
        case others:
        // ...
    }
}

这种方式相对于哨兵来说,可以包含更加丰富的信息,但是同样也将错误的类型暴露给了外部,例如标准库中的 os.PathError

结论

不建议使用,或者至少不能用于公共API。

Opaque errors

不透明的错误处理,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。虽然调用者知道发生了错误,但调用者没有能力看到错误的内部。作为调用者,关于操作的结果,只需指定成功还是失败。这就是不透明错误处理的全部功能–只需返回错误而不假设其内容

  • 被调用者可随意向error增添更多的信息,而不会影响调用者处理逻辑。

在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}
  • 对错误的判断封装到底层,通过

  • 判断接口和error解耦,不同的error自行实现接口,error甚至可以和判断接口定义在不同的包中,达到error行为和error本身的解耦,很巧妙。

  • 这种方式最大的特点就是只返回错误,暴露错误判定接口,不返回类型,这样可以减少 API 的暴露,后续的处理会比较灵活,这个一般用在公共库会比较好。

  • 被调用者可随意向error增添更多的信息,而不会影响调用者处理逻辑。

结论

推荐使用opaque error,实在需要细化判断error,也只能判断他的行为,而不是类型和值。判断行为时使用github.com/pkg/errors/errors.go#Cause()获取原始错误。

Go 1.13 errors

errors.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
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}
  • reflectlite.TypeOf(target).Comparable():先判断是否可通过==比较

    • Comparable():判断equal属性是否为空,equal是一个方法。

      func (t *rtype) Comparable() bool {
      return t.equal != nil
      }
      
      type rtype struct {
      size       uintptr
      ptrdata    uintptr // number of bytes in the type that can contain pointers
      hash       uint32  // hash of type; avoids computation in hash tables
      tflag      tflag   // extra type information flags
      align      uint8   // alignment of variable with this type
      fieldAlign uint8   // alignment of struct field with this type
      kind       uint8   // enumeration for C
      // function for comparing objects of this type
      // (ptr to object A, ptr to object B) -> ==?
      equal     func(unsafe.Pointer, unsafe.Pointer) bool
      gcdata    *byte   // garbage collection data
      str       nameOff // string form
      ptrToThis typeOff // type for pointer to this type, may be zero
      }
    • 指针变量默认比较两个指针内存地址。

      image-20220806101750565

    • 仅包含string字段的结构体变量,equal为runtime.strequal

      image-20220806124903356

      《go语言精进之路》里string章节详细讲了string比较,后面会分享笔记。

    • 包含多种类型的结构体变量,equal为type..eq.main.MyError

      type MyError struct {
      a   int
      msg string
      }

      image-20220806125153472

      结构体变量比较:结构体字段逐个比较。推测这里的的equal表示这个意思。

    • map、slience 不支持==比较,可以使用反射包里的reflect.DeepEqual,不过性能差一些。如果是单元测试,可使用google/go-cmp。无法比较时会panic,故非测试代码使用时要注意。

      吐槽下go的==比较,是运行时go根据具体类型做不同的操作,没有在源码中体现。而java的比较更清晰,例如字符串比较,可以看到string源码重写的equals方法。

  • if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target):断言是否实现Is(error) bool接口,实现就直接调用。

  • err = Unwrap(err):递归解开error。有时需要通过err传递更多信息,就会对error进行包裹。

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

errors.As()

errors.As(a,b):通过反射判断a是否和b是一个类型,如果是就把b赋值给a,如果不是,unwrap a,重复前面的步骤。

wrap error

errors/wrap.go

image-20220811102437415

前一篇文章分析了error标准库里的error.Is(), error.As(),如果要使用这套逻辑来wrap error,以达到携带更多上下文信息的目的。在做error判断时,需要自行实现Unwrap接口,才能解开已经wrap的error。想想还是挺复杂的,要自定义error,每个自定义error还得去实现Unwrap接口。

github.com/pkg/errors/errors.go

image-20220811103110142

  • 下面的As, Is, Unwrap是直接调用标准库的代码,kratos也是这么干的,推测这样做的好处是:需要处理错误时,只需导入github.com/pkg/errors包即可,而不用再导入同包名的标准库error包(需要做别名处理),不再纠结导入哪个error包。

github.com/pkg/errors使用起来很方便,可以解决上诉问题。关键代码是Wrap(), Cause()。前者用来wrap error,可携带额外信息和堆栈。后者用来解开已经wrap的error,得到最原始的error。

  • 底层包不应wrap error,应该返回原始错误。
  • 得到原始错误时,第一时间wrap,保留堆栈。
  • 可为error增加更多的上下文信息,例如入参。

VS标准库的 fmt.Errorf("%w")

我们先看一下标准库的源码,我们可以发现当 p.wrappedErr != nil 的时候(也就是有 %w)的时候,会使用一个 wrapError 将错误包装,看 wrapError 的源码可以发现,这个方法只是包装了一下原始错误,并且可以做到附加一些文本信息,但是没有堆栈信息。

func Errorf(format string, a ...interface{}) 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
}
Copy

在看一下 pkg/errors 的源码,我肯可以发现除了使用 withMessage 附加了错误信息之外还使用 withStack 附加了堆栈信息,这样我们在程序入口处打印日志信息的时候就可以将堆栈信息一并打出了

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
    if err == nil {
        return nil
    }
    err = &withMessage{
        cause: err,
        msg:   message,
    }
    return &withStack{
        err,
        callers(),
    }
}

handle error gracefully

封装error到对象属性,简化主干代码

bufio.scan

对比下面两个函数的处理我们可以发现, count2 使用 sc.Scan 之后一个 if err 的判断都没有,极大的简化了代码,这是因为在 sc.Scan 做了很多处理,像很多类似的,需要循环读取的都可以考虑像这样包装之后进行处理,这样外部包调用的时候就会非常简洁。

// 统计文件行数
func count(r io.Reader) (int, error) {
    var (
        br    = bufio.NewReader(r)
        lines int
        err   error
    )

    for {
        // 读取到换行符就说明是一行
        _, err = br.ReadString('\n')
        lines++
        if err != nil {
            break
        }
    }

    // 当错误是 EOF 的时候说明文件读取完毕了
    if err != io.EOF {
        return 0, err
    }

    return lines, err
}

func count2(r io.Reader) (int, error) {
    var (
        sc    = bufio.NewScanner(r)
        lines int
    )

    for sc.Scan() {
        lines++
    }

    return lines, sc.Err()
}
  • 这里将error封装到底层
  • sc.Err():将error作为字段,链式调用时很有用,gorm就是这么干的。

error writer

看一个来自 go blog 的例子:https://blog.golang.org/errors-are-values
一般代码

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

errWriter

type errWriter struct {
    w   io.Writer
    err error
}

func (ew *errWriter) write(buf []byte) {
    if ew.err == nil {
        _, ew.err = ew.w.Write(buf)
    }    
}

// 使用时
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}
  • 如果去翻 标准库中 bufio.Writer 的源代码,你会发现也有这种用法,这种就是将重复的逻辑进行了封装,然后把 error 暂存,然后我们就只需要在最后判断一下 error 就行了。
  • 多次调用同一方法,并需要判断error的场景下适用。

参考

  1. Go错误处理最佳实践
  2. Don’t just check errors, handle them gracefully
作者:Yuyy
博客:https://yuyy.info
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇