设为首页 加入收藏

TOP

Golang error 的突围(二)
2019-09-18 11:10:41 】 浏览:156
Tags:Golang error 突围
EOF,表示“文件结束”错误。但是这种方式处理起来,不太灵活:

func main() {
    r := bytes.NewReader([]byte("0123456789"))
    
    _, err := r.Read(make([]byte, 10))
    if err == io.EOF {
        log.Fatal("read failed:", err)
    }
}

必须要判断 err 是否和约定好的错误 io.EOF 相等。

再来一个例子,当我想返回 err 并且加上一些上下文信息时,就麻烦了:

func main() {
    err := readfile(“.bashrc”)
    if strings.Contains(error.Error(), "not found") {
        // handle error
    }
}

func readfile(path string) error {
    err := openfile(path)
    if err != nil {
        return fmt.Errorf(“cannot open file: %v", err)
    }
    // ……
}

readfile 函数里判断 err 不为空,则用 fmt.Errorf 在 err 前加上具体的 file 信息,返回给调用者。返回的 err 其实还是一个字符串。

造成的后果时,调用者不得不用字符串匹配的方式判断底层函数 readfile 是不是出现了某种错误。当你必须要这样才能判断某种错误时,代码的“坏味道”就出现了。

顺带说一句,err.Error() 方法是给程序员而非代码设计的,也就是说,当我们调用 Error 方法时,结果要写到文件或是打印出来,是给程序员看的。在代码里,我们不能根据 err.Error() 来做一些判断,就像上面的 main 函数里做的那样,不好。

Sentinel errors 最大的问题在于它在定义 error 和使用 error 的包之间建立了依赖关系。比如要想判断 err == io.EOF 就得引入 io 包,当然这是标准库的包,还 Ok。如果很多用户自定义的包都定义了错误,那我就要引入很多包,来判断各种错误。麻烦来了,这容易引起循环引用的问题。

因此,我们应该尽量避免 Sentinel errors,仅管标准库中有一些包这样用,但建议还是别模仿。

第二种就是 Error Types,它指的是实现了 error 接口的那些类型。它的一个重要的好处是,类型中除了 error 外,还可以附带其他字段,从而提供额外的信息,例如出错的行数等。

标准库有一个非常好的例子:

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

PathError 额外记录了出错时的文件路径和操作类型。

通常,使用这样的 error 类型,外层调用者需要使用类型断言来判断错误:

// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
    switch err := err.(type) {
    case *PathError:
        return err.Err
    case *LinkError:
        return err.Err
    case *SyscallError:
        return err.Err
    }
    return err
}

但是这又不可避免地在定义错误和使用错误的包之间形成依赖关系,又回到了前面的问题。

即使 Error typesSentinel errors 好一些,因为它能承载更多的上下文信息,但是它仍然存在引入包依赖的问题。因此,也是不推荐的。至少,不要把 Error types 作为一个导出类型。

最后一种,Opaque errors。翻译一下,就是“黑盒 errors”,因为你能知道错误发生了,但是不能看到它内部到底是什么。

譬如下面这段伪代码:

func fn() error {
    x, err := bar.Foo()
    if err != nil {
        return err
    }
    
    // use x
    return nil
}

作为调用者,调用完 Foo 函数后,只用知道 Foo 是正常工作还是出了问题。也就是说你只需要判断 err 是否为空,如果不为空,就直接返回错误。否则,继续后面的正常流程,不需要知道 err 到底是什么。

这就是处理 Opaque errors 这种类型错误的策略。

当然,在某些情况下,这样做并不够用。例如,在一个网络请求中,需要调用者判断返回的错误类型,以此来决定是否重试。这种情况下,作者给出了一种方法:

In this case rather than asserting the error is a specific type or value, we can assert that the error implements a particular behaviour.

就是说,不去判断错误的类型到底是什么,而是去判断错误是否具有某种行为,或者说实现了某个接口。

来个例子:

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}

拿到网络请求返回的 error 后,调用 IsTemporary 函数,如果返回 true,那就重试。

这么做的好处是在进行网络请求的包里,不需要 import 引用定义错误的包。

handle not just check errors

这一节要说第二句箴言:“Don't just check errors, handle them gracefully”。

func AuthenticateRequest(r *Request) error {
     err := authenticate(r.User)
     if err != nil {
        return err
     }
     return nil
}

上面这个例子中的代码是有问题的,直接优化成一句就可以了:

func AuthenticateRequest(r *Request) error {
     return authenticate(r.User)
}

还有其他的问题,在函数调用链的最顶层,我们得到的错误可能是:No such file or directory

这个错误反馈的信息太少了,不知道文件名、路径、行号等等。

尝试改进一下,增加一点上下文:

func AuthenticateRequest(r *Request) error {
     err := authenticate(r.User)
     if err != nil {
        return fmt.Errorf("authenticate failed: %v", err)
     }
     return nil
}

这种做法实际上是先错误转换成字符串,再拼接另一个字符串,最后,再通过 fmt.Errorf 转换成错误。这样做破坏了相等性检测,即我们无法判断错误是否是一种预先定义好的错误了。

应对方案是使用第三方库:github.com/pkg/errors。提供了友好的界面:

// Wrap a
首页 上一页 1 2 3 4 5 下一页 尾页 2/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇golang基础语法 下一篇Go Modules使用教程

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目