前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 进阶训练营 – 错误处理二:错误定义与处理

Go 进阶训练营 – 错误处理二:错误定义与处理

作者头像
Yuyy
发布2022-09-13 09:10:33
6880
发布2022-09-13 09:10:33
举报
文章被收录于专栏:yuyy.info技术专栏

error type: 错误定义与判断

Sentinel Error

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

代码语言:javascript
复制
// 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 进行判断。

代码语言:javascript
复制
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 的接口,然后在外部是否类型断言来判断是否是这种错误类型

代码语言:javascript
复制
type MyStruct struct {
    s string
    name string
    path string
}

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

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

结论

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

Opaque errors

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

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

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

代码语言:javascript
复制
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()

代码语言:javascript
复制
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
        }
    }
}

errors.As()

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

wrap error

errors/wrap.go

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

github.com/pkg/errors/errors.go

  • 下面的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 的源码可以发现,这个方法只是包装了一下原始错误,并且可以做到附加一些文本信息,但是没有堆栈信息。

代码语言:javascript
复制
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 附加了堆栈信息,这样我们在程序入口处打印日志信息的时候就可以将堆栈信息一并打出了

代码语言:javascript
复制
// 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 做了很多处理,像很多类似的,需要循环读取的都可以考虑像这样包装之后进行处理,这样外部包调用的时候就会非常简洁。

代码语言:javascript
复制
// 统计文件行数
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 一般代码

代码语言:javascript
复制
_, 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

代码语言:javascript
复制
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

Post Views: 7

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-8-27 1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • error type: 错误定义与判断
    • Sentinel Error
      • 结论
    • error types
      • 结论
    • Opaque errors
      • 结论
  • Go 1.13 errors
    • errors.Is()
      • errors.As()
      • wrap error
        • errors/wrap.go
          • github.com/pkg/errors/errors.go
            • VS标准库的 fmt.Errorf("%w")
            • bufio.scan
            • error writer
        • handle error gracefully
        • 参考
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档