Go Error 处理


Go 中关于错误的哲学,应该尽可能优雅地处理错误。

先说结论:

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

error 的类型是 interface

type error interface {
    Error() string
}

创建error:

// example 1
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

// example 2
if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

自定义error:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

自定义 error 类型可以拥有一些附加方法。比如 net.Error 定义如下:

package net

type Error interface {
    error
    // Is the error a timeout
    Timeout() bool
    // Is the error temporary
    Temporary() bool
}

网络客户端程序代码可以使用类型断言判断网络错误是瞬时错误还是永久错误。
比如,一个网络爬虫可以在碰到瞬时错误的时候,等待一段时间然后重试。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

使用 Wrap 和 stack traces。

package main

import (
    "fmt"
    
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("error")
    err = errors.Wrap(err, "open failed")
    err = errors.Wrap(err, "read config failed")
    // read config failed: open failed: error
    fmt.Println(err)
}

Wrap error 到 context,并添加到 base error 中,并记录发生 error 的文件和行。这个文件和行信息可以通过一个 helper Fprint 检索,以提供 error trace。

Fprint 可以用 Printf 替代。

func main() {
    err := parseArgs(os.Args[1:])
    fmt.Printf("%+v\n", err)
}

如果需要控制 Cause 和 StackTrace,可以参照如下代码:

func parseArgs(args []string) error {
    if len(args) < 3 {
        return errors.Errorf("not enough arguments, expected at least 3, got %d", len(args))
    }
}

如果从另一个函数接收到错误,通常只需返回它就足够了

if err != nil {
    return err
}

如果 open file 调用可以注明 path。

f, err := os.Open(path)
if err != nil {
    return errors.Wrapf(err, "failed to open %q", path)
}

是将错误返回给调用者,而不是在整个程序中记录它们。在程序的顶层或 worker goroutine,可以使用 %+v 以足够详细的方式打印错误。

func main() {
    err := app.Run()
    if err != nil {
        fmt.Printf("FATAL: %+v\n", err)
        os.Exit(1)
    }
}

从 error 中提取 contextor,最常见的 error 处理方法可能是日志记录。在使用结构化记录时,信息通常表示为一组 kv。此时,我们不想再处理特定的 error,因此我们需要一个通用的解决方案来从错误中提取数据。

type contextor interface {
    Context() map[string]interface{}
}

func Context(err error) map[string]interface{} {
    ctx := new(map[string]interface{})
    if e, ok := err.(contextor); ok {
        ctx = e.Context()
    }
    return ctx
}

error interface 的 Error 方法的输出,是给人看的,不是给机器看的。我们通常会把 Error 方法返回的字符串打印到日志中,或者显示在控制台上。永远不要通过判断 Error 方法返回的字符串是否包含特定字符串,来决定错误处理的方式。

如果你是开发一个公共库,库的 API 返回了特定值的错误值。那么必须把这个特定值的错误定义为 public,写在文档中。

自定义错误类型:

// 定义错误类型
type MyError struct {
    Msg string
    File string
    Line int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}

// 被调用函数中
func doSomething() {
    // do something
    return &MyError{"Something happened", "server.go", 42}
}

// 调用代码中
err := doSomething()
// 使用 类型断言 获得错误详细信息
if err, ok := err.(SomeType); ok {
    //...
}

这种方式相比于"返回和检查错误值",很大一个优点在于可以将 底层错误包起来一起返回给上层,这样可以提供更多的上下文信息。比如 os.PathError:

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

func (e *PathError) Error() string

然而,这种方式依然会增加模块之间的依赖。

隐藏内部细节的错误处理。这种策略之所以叫"隐藏内部细节的错误处理",是因为当上层代码碰到错误发生的时候,不知道错误的内部细节。作为上层代码,你需要知道的就是被调用函数是否正常工作。如果你接受这个原则,将极大降低模块之间的耦合性。

使用 errors 包来重新定义 error。

首先是 Wrap,输入 error 和 message 并产生一个新的 error。

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

然后是 Cause,它获取一个可能已被包装的 error,并将其展开以恢复原始 error。

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

使用这两个函数,我们现在可以定义任何错误,并在需要检查时恢复基础错误。考虑这个将文件内容读入内存的函数示例。

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
}

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)
    }
}

输出:

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

第一行是 ReadConfig 的报错,第二行是 ReadFile 的报错,最后是包本身的报错。

在操作中,需要检查 error 是否匹配特定的值或类型时,应该首先使用 errors.Cause 恢复 base error。

type causer interface {
    Cause() error
}
func Cause(err error) error {
    if e, ok := err.(causer); ok {
        return e.Cause()
    }
    return nil
}

通过这种方式,任何实现该接口的错误都可以暴露原始错误以供进一步检查:

func (e *authorizationError) Cause() error {
    return e.err
}

当然,我们可能需要不止一次地包装错误,所以只要稍加修改,我们就可以使我们的 Cause 函数返回错误的根本原因:

func Cause(err error) error {
    for err != nil {
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return err
}

在 error 中使用 logrus:

err := doSomething()
logrus.WithFields(logrus.Fields(Context(err))).Error(err)

结合前面示例中的 causer 接口,我们可以从每个包装错误中收集上下文:

func Context(err error) map[string]interface{} {
    ctx := make(map[string]interface{})
    for err != nil {
        if e, ok := err.(contextor); ok {
            for key, value := range e.Context() {
                ctx[key] = value
            }
        }
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return ctx
}

组合使用:

package main

import (
    "fmt"
    "os"

    "github.com/pkg/errors"
)

type stackTracer interface {
    StackTrace() errors.StackTrace
}

func main() {
    err := bar()
    if err != nil {
        // Output: bar went wrong: foo went wrong
        fmt.Println(err)
        if err, ok := err.(stackTracer); os.Getenv("DEBUG") != "" && ok {
            fmt.Printf("%+v", err.StackTrace()[0:2]) // top two frames
        }
    }
}

type fooError struct{}

func (*fooError) Error() string {
    return "foo went wrong"
}

func foo() error {
    return &fooError{}
}

func bar() error {
    err := foo()
    if err != nil {
        return errors.Wrap(err, "bar went wrong")
    }
    return nil
}
分享:

评论