go errors


原文链接: go errors

Go语言中的错误处理(Error Handling in Go)
更优雅的 Golang 错误处理

使用“隐藏内部细节的错误处理”
使用errors.Wrap封装原始error
使用errors.Cause找出原始error
为了行为而断言,而不是类型
尽量减少错误值的使用

  • Annotating errors

我想建议一种方法来为错误添加上下文,并且我会介绍一个简单的包。该代码在:http://github.com/pkg/errors。错误包有两个主要功能:

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error

第一个函数是 Wrap,它接受一个 error 接口类型和一个 string,并返回新的 error 接口类型。

// Cause unwraps an annotated error.
func Cause(err error) error

第二个函数是 Cause,它接受可能被包装的错误,并解开它以恢复原始错误。使用这两个函数,我们现在可以注释任何错误,并在需要检查时恢复基础错误。考虑一个将文件内容读入内存的函数示例:

func ReadFile(path string) ([]byte, error) {
        f, err := os.Open(path)
        if err != nil {
                return nil, errors.Wrap(err, "open failed")
        }
        defer f.Close()

        buf, err := ioutil.ReadAll(f)
        if err != nil {
                return nil, errors.Wrap(err, "read failed")
        }
        return buf, nil
}

我们将使用这个函数来编写一个函数来读取一个配置文件,然后调用它在 main 函数里面。

func ReadConfig() ([]byte, error) {
        home := os.Getenv("HOME")
        config, err := ReadFile(filepath.Join(home, ".settings.xml"))
        return config, errors.Wrap(err, "could not read config")
}

func main() {
        _, err := ReadConfig()
        if err != nil {
                fmt.Println(err)
                os.Exit(1)
        }
}

如果 ReadConfig 代码路径失败,因为我们使用 errors.Wrap 了 K&D 风格,所以我们得到了一个很好的带注释的错误。

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

由于 errors.Wrap 产生了一堆错误,我们可以检查该堆栈以获取更多调试信息。这也是同样的例子,但这次我们用 errors.Print 换 fmt.Println。

func main(){
        _,err:= ReadConfig()
        如果err!= nil {
                errors.Print(ERR)
                os.Exit(1)
        }
}

我们会得到这样的东西:

readfile.go:27: could not read config
readfile.go:14: open failed
open /Users/dfc/.settings.xml: no such file or directory

第一行来自 ReadConfig,第二来自 os.Open 的一部分 ReadFile,剩余部分来自于 os 包本身,其不携带位置信息。我们引入了包装错误的概念来生成一个堆栈,现在我们需要讨论相反的事情,结构 error,这是该 errors.Cause 功能的领域。
为了行为断言错误,而非为了类型
在有些场景下,仅仅知道是否出错是不够的。比如,和进程外其它服务通信,需要了解错误的属性,以决定是否需要重试操作。

这种情况下,就不要判断错误值或者错误的类型(如os.PathError)了,我们可以判断错误是否实现某个行为。

type temporary interface {
    Temporary() bool   
}
 // IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
    te, ok := errors.Cause(err).(temporary)
    return ok && te.Temporary()
}

这种实现方式的好处在于,不需要知道具体的错误类型,也就不需要引用定义了错误类型的三方package。如果你是底层代码的开发者,哪天你想更换一个实现更好的error,也不用担心影响上层代码逻辑。如果你是上层代码的开发者,你只需要关注error是否实现了特定行为,不用担心引用的三方package升级后,程序逻辑失败。
在操作中,无论何时需要检查错误与特定值或类型相匹配时,都应首先使用该 errors.Cause 功能恢复原始错误。

  • Only handle errors once

最后,我想提一提,你应该只处理一次错误。处理错误意味着检查错误值并做出决定。

func Write(w io.Writer, buf []byte) {
        w.Write(buf)
}

如果你做出的决定少于一个,你会忽略错误。正如我们在这里看到的那样,错误来自 w.Write 被抛弃。但是,针对单个错误做出多个决定也是有问题的,比如:

func Write(w io.Writer, buf []byte) error {
        _, err := w.Write(buf)
        if err != nil {
                // annotated error goes to log file
                log.Println("unable to write:", err)

                // unannotated error returned to caller
                return err
        }
        return nil
}

在这个例子中,如果在执行期间发生错误,则会 Write 将一行写入日志文件,注意发生错误的文件和行,并且错误也会返回给调用者,调用者可能会将其记录并返回,回到程序的顶端。因此,你会在日志文件中获得一堆重复行,但是在程序的顶部,没有得到任何上下文的原始错误。anyone?

func Write(w io.Write, buf []byte) error {
        _, err := w.Write(buf)
        return errors.Wrap(err, "write failed")
}

使用该 http://github.com/pkg/errors 软件包,你可以通过人员和机器均可检查的方式向错误值添加上下文

  • 结论

简而言之,错误是你的软件包的公共 API 的一部分,与你公开 API 的任何其他部分一样谨慎。为了获得最大的灵活性,我建议你尝试将所有错误视为不透明。在不能这样做的情况下,为行为声明错误,而不是类型或值。最大限度地减少程序中的标记错误值的数量,并将错误转换为不透明的错误,只要它们一出现就用 errors.Wrap 将它们包装起来。最后,如果需要检查它,则使用 errors.Cause 来恢复基本错误。

原文:

Don’t just check errors, handle them gracefully​dave.cheney.net

`