Golang错误包装机制解析与应用

Golang错误包装机制解析与应用 我希望有一种方法可以返回错误值而不丢失任何更深层的错误信息。例如,在我的数据库代码中,当获取操作失败时,我会返回一个错误,例如 ErrDatabaseGetFailed。这与存储类型(bolt/inmem 等)无关,但我仍然希望返回具体的失败原因,例如 boltdb/inmem 等出现了问题。

我希望能够实现类似这样的操作:

if err == MyErr

if err == SpecificErr

或者/并且获取更多信息进行日志记录。然而,Go 只有 fmt 的错误包装功能,我无法在同一个错误中返回一个我已定义的现有错误值以及更深层的错误,因此我想出了以下方案。

type wrappedError struct {
	innerErr error
	outerErr error
}

func (e *wrappedError) Error() string { return e.outerErr.Error() }

func (e *wrappedError) Is(v error) bool { return v == e.outerErr }

func (e *wrappedError) Unwrap() error { return e.innerErr }

func (e *wrappedError) As(target interface{}) bool {
	return errors.As(e.outerErr, target)
}

func Wrap(outerErr error, innerErr error) error {
	return &wrappedError{outerErr: outerErr, innerErr: innerErr}
}

这使我能够通过以下方式简单地返回一个错误层次结构,包括自定义类型和错误值:

return Wrap(ErrDatabaseGetFailed, err)

包装后的错误被视为 ErrDatabaseGetFailed。你不能使用 == 进行比较,但无论如何这可能不是一个好主意,尽管我承认直到最近我一直在这样做。

相反,应该使用:

errors.Is(err, ErrDatabaseGetFailed)
errors.Is(err, ErrFromBoltDB)

为什么 Go 的内置错误包没有类似的功能呢?


更多关于Golang错误包装机制解析与应用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我认为这类似于错误包装:在 Go 1.13 中处理错误 - go.dev

更多关于Golang错误包装机制解析与应用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在他们的示例中:

return fmt.Errorf("access denied: %w", ErrPermission)

这仅允许我用额外的信息来注释一个现有的错误。我希望能够从一个错误中返回另一个错误,以指示错误的来源。例如,我的数据库代码是通用的,我持久化一个包含数据字节切片的数据类型,在更高层次上,每个仓库类型都使用这段代码。通过在一个错误中返回另一个错误,我可以精确追踪该错误的来源,但我也可以让自定义错误实现一个 Message() 方法,为我提供可能导致该错误的输入信息的日志记录。我可以在我的日志记录中间件中完成这项工作,并在返回时将其附加上去。

为什么Go语言内置的错误包没有类似的功能呢?

或许目前还没有一个公认的“好”方法来实现这一点,因此Go标准库中没有内置标准方式。

例如…

我是在学习了Python和C#之后才学习Go的,所以我编写了自己的 errors 包,它包含了Python中 __cause____context__ 的概念,因此我的错误类型本质上是:

type myError struct {
    err error
    cause error
    context error
}

其中 errcause 的行为分别类似于你的 outerErrinnerErr,但还有一个额外的 context 字段,用于记录尝试处理错误时遇到的任何错误。

后来,我为另一个项目编写了另一个 errors 子包,它取消了原因(cause)和上下文(context)的区分,取而代之的是一个 []error 切片,其第一个错误是最外层的错误,最后一个是最内层的错误。

这种实现方式的一些优点是:

  • 对于处理来自多个并发goroutine的扇出和扇入结果的函数,我更容易表示其错误。
  • 在遍历深层嵌套的错误时,与遍历行为类似于链表(或在使用 errors.Is/errors.As 遍历时类似于树)的、包含两个错误字段的结构体相比,它具有更好的性能特征。

然而,这种方式的一个主要缺点是,包装错误本质上执行了以下操作:

func wrap(outer error, inner error) error {
    switch inner := inner.(type) {
    // ...
    case myCustomErrorSlice:
        newErrors := make(myCustomErrorSlice, len(inner) + 1)
        copy(newErrors[1:], inner)
        newErrors[0] = outer
        return newErrors
    // ...
    }
}

分配和复制操作可能很慢,并且如果某些东西使那些内部错误切片保持活动状态,可能会导致大量内存使用。

总结:

我的观点是,生成错误并将错误传播给调用者有很多种方法,它们各有权衡取舍,可能需要不同的用户编写自己的错误类型,以不同的方式解决问题。errors.Aserrors.Is 使得可以一起使用这些不同的错误类型。

Go 1.13 引入的错误包装机制已经提供了你需要的功能。fmt.Errorf 配合 %w 动词可以包装错误,同时 errors.Iserrors.As 能够处理错误链的检查和类型断言。

以下是标准库的实现方式:

import (
    "errors"
    "fmt"
)

var ErrDatabaseGetFailed = errors.New("database get failed")

func queryDatabase() error {
    // 模拟底层数据库错误
    err := errors.New("bolt: transaction closed")
    
    // 包装错误,保留原始错误信息
    return fmt.Errorf("%w: %v", ErrDatabaseGetFailed, err)
}

func main() {
    err := queryDatabase()
    
    // 检查错误链中是否包含特定错误
    if errors.Is(err, ErrDatabaseGetFailed) {
        fmt.Println("捕获到数据库获取失败错误")
    }
    
    // 获取原始错误信息
    fmt.Printf("完整错误: %v\n", err)
    fmt.Printf("解包错误: %v\n", errors.Unwrap(err))
}

对于需要自定义错误类型的场景,可以这样实现:

type DatabaseError struct {
    Op   string
    Err  error
    Code int
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error %s: %v", e.Op, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

func queryWithCustomError() error {
    baseErr := errors.New("connection timeout")
    return &DatabaseError{
        Op:   "get",
        Err:  baseErr,
        Code: 500,
    }
}

func main() {
    err := queryWithCustomError()
    
    // 使用 errors.As 进行类型断言
    var dbErr *DatabaseError
    if errors.As(err, &dbErr) {
        fmt.Printf("操作: %s, 代码: %d\n", dbErr.Op, dbErr.Code)
    }
    
    // 检查底层错误
    if errors.Is(err, errors.New("connection timeout")) {
        fmt.Println("根本原因是连接超时")
    }
}

标准库的设计考虑了以下因素:

  1. 保持错误处理的简洁性
  2. 通过 errors.Iserrors.As 提供可靠的错误检查
  3. 使用 fmt.Errorf%w 提供简单的错误包装
  4. 通过 Unwrap 接口支持自定义错误链

你的 wrappedError 实现与标准库理念相似,但标准库方案更统一且被广泛接受。使用 == 直接比较错误在包装场景下确实不可靠,errors.Is 是更安全的替代方案。

回到顶部