Go 错误:Is() 和 As() 声称是递归的,是否有任何类型实现了错误接口并支持这种递归 - 没有错误?

Go errors: Is() and As() claim to be recursive, is there any type that implements the error interface and supports this recursion - bug free?

在我看来,在 Go 中“包装”错误的“方法”是使用 fmt.Errof 和 %w 动词

https://go.dev/blog/go1.13-errors

但是,fmt.Errorf 不会递归地包装错误。无法使用它来包装三个先前定义的错误(Err1、Err2 和 Err3),然后使用 Is() 检查结果并为这三个错误中的每一个都获得 true。

最终编辑:

感谢 和下面的评论,我现在有一个简单的方法来实现它(尽管,我仍然很好奇是否有一些标准类型可以做到这一点)。由于没有示例,我创建示例的尝试失败了。我缺少的部分是在我的类型中添加 IsAs 方法。因为自定义类型需要包含错误和指向下一个错误的指针,所以自定义 IsAs 方法允许我们比较自定义类型中包含的错误,而不是自定义类型本身。

这是一个工作示例:https://go.dev/play/p/6BYGgIb728k

以上要点link

type errorChain struct {
    err  error
    next *errorChain
}

//These two functions were the missing ingredient
//Defined this way allows for full functionality even if
//The wrapped errors are also chains or other custom types

func (c errorChain) Is(err error) bool { return errors.Is(c.err, err) }

func (c errorChain) As(target any) bool { return errors.As(c.err, target) }

//Omitting Error and Unwrap methods for brevity

func Wrap(errs ...error) error {
    out := errorChain{err: errs[0]}

    n := &out
    for _, err := range errs[1:] {
        n.next = &errorChain{err: err}
        n = n.next
    }
    return out
}

var Err0 = errors.New("error 0")
var Err1 = errors.New("error 1")
var Err2 = errors.New("error 2")
var Err3 = errors.New("error 3")

func main() {
    //Check basic Is functionality
    errs := Wrap(Err1, Err2, Err3)
    fmt.Println(errs)                            //error 1: error 2: error 3
    fmt.Println(errors.Is(errs, Err0))           //false
    fmt.Println(errors.Is(errs, Err2))           //true
}

虽然 Go source specifically mentions the ability to define an Is method, the example 没有以可以解决我的问题的方式实现它,并且讨论没有立即明确表明需要利用 errors.Is 的递归性质.

现在回到原来的状态 POST:

Go 中是否有内置的东西可以正常工作?

我尝试自己制作一个(几次尝试),但 运行 遇到了不希望的问题。这些问题源于这样一个事实,即 Go 中的错误似乎是通过地址进行比较的。即,如果 Err1 和 Err2 指向同一事物,则它们是相同的。

这给我带来了麻烦。我可以天真地让 errors.Iserrors.As 以递归方式处理自定义错误类型。很简单。

  1. 创建一个实现错误接口的类型(有一个 Error() string 方法)
  2. 该类型必须有一个代表包装错误的成员,该成员是指向其自身类型的指针。
  3. 实施 Unwrap() error 方法来 return 封装错误。
  4. 实现一些方法,将一个错误包装到另一个错误中

好像还不错。但是有麻烦了。

因为错误是指针,如果我做类似 myWrappedError = Wrap(Err1, Err2) 的事情(在这种情况下假设 Err1Err2 包裹)。不仅 errors.Is(myWrappedError, Err1)errors.Is(myWrappedError, Err2) return 是正确的,errors.Is(Err2, Err1)

也是正确的

如果需要生成 myOtherWrappedError = Wrap(Err3, Err2) 并稍后调用 errors.Is(myWrappedError, Err1) 它现在将 return false!进行 myOtherWrappedError 更改 myWrappedError.

我尝试了几种方法,但总是运行进入相关问题。

这可能吗?是否有执行此操作的 Go 库?

注意:我更感兴趣的可能是已经存在的正确方法,而不是我的基本尝试的具体错误

编辑 3:正如其中一个答案所建议的,我的第一个代码中的问题显然是我修改了全局错误。我知道,但未能充分沟通。下面,我将包含其他不使用指针且不修改全局变量的损坏代码。

编辑 4:稍微修改以使其更有效,但它仍然有问题

https://go.dev/play/p/bSytCysbujX

type errorGroup struct {
    err        error
    wrappedErr error
}

//...implemention Unwrap and Error excluded for brevity

func Wrap(inside error, outside error) error {
    return &errorGroup{outside, inside}
}

var Err1 = errorGroup{errors.New("error 1"), nil}
var Err2 = errorGroup{errors.New("error 2"), nil}
var Err3 = errorGroup{errors.New("error 3"), nil}

func main() {
    errs := Wrap(Err1, Err2)
    errs = Wrap(errs, Err3)
    fmt.Println(errs)//error 3: error 2: error 1
    fmt.Println(errors.Is(errs, Err1)) //true
    fmt.Println(errors.Is(errs, Err2)) //false <--- a bigger problem
    fmt.Println(errors.Is(errs, Err3)) //false <--- a bigger problem
}

编辑 2:缩短了 playground 版本

有关此示例,请参阅 https://go.dev/play/p/swFPajbMcXA

编辑 1:我的代码的精简版侧重于重要部分:

type errorGroup struct {
    err        error
    wrappedErr *errorGroup
}

//...implemention Unwrap and Error excluded for brevity

func Wrap(errs ...*errorGroup) (r *errorGroup) {
    r = &errorGroup{}
    for _, err := range errs {
        err.wrappedErr = r
        r = err

    }
    return
}

var Err0 = &errorGroup{errors.New("error 0"), nil}
var Err1 = &errorGroup{errors.New("error 1"), nil}
var Err2 = &errorGroup{errors.New("error 2"), nil}
var Err3 = &errorGroup{errors.New("error 3"), nil}

func main() {
    errs := Wrap(Err1, Err2, Err3)//error 3: error 2: error 1
    fmt.Println(errors.Is(errs, Err1)) //true

    //Creating another wrapped error using the Err1, Err2, or Err3 breaks the previous wrap, errs.
    _ = Wrap(Err0, Err2, Err3)
    fmt.Println(errors.Is(errs, Err1)) //false <--- the problem
}

您的代码修改了 package-global 个错误值,因此它本质上是错误的。此缺陷与 Go 的错误处理机制无关。

根据您链接的文档,有两个 error-handling 助手:IsAsIs 允许您递归地展开错误,查找特定错误 value,这必须是全局包才能有用。另一方面,As 允许您递归解包错误以查找给定 type.

的任何已包装错误值

包装是如何工作的?您将错误 A 包装在 新错误值 B 中。 Wrap() 助手将 必然 return 一个新值 ,因为fmt.Errorf 在链接文档的示例中执行。 Wrap 助手应该 永远不会 修改被包装的错误的值。该值应被视为不可变的。事实上,在任何正常的实现中,该值都是 error 类型的,因此您可以包装 any 错误,而不是仅仅将自定义错误类型的同心值包装在彼此;并且,在这种情况下,您无权访问包装错误的字段来修改它们。本质上,Wrap 应该大致是:

func Wrap(err error) error {
    return &errGroup{err}
}

就是这样。这不是很有用,因为 errGroup 的实现实际上并没有做任何事情——它没有提供有关发生的错误的详细信息,它只是其他错误的容器。为了使它有价值,它应该有一个 string 错误消息,或者像其他一些错误类型的方法 IsNotFound,或者比仅仅使用 error 和 [=] 更有用的东西16=].

根据您示例代码中的用法,您似乎还假设用例是说“我想将 A 包装在 C 中的 B”中,这是我在野外从未见过的,我想不出任何需要它的场景。包装的目的是说“我收到了错误 A,我将把它包装在错误 B 中以添加上下文,然后 return 它”。调用者可能会在错误 C 中包装 that 错误,依此类推,这就是递归包装有价值的原因。

例如:https://go.dev/play/p/XeoONx19dgX

你可以这样使用:

type errorChain struct {
    err  error
    next *errorChain
}

func Wrap(errs ...error) error {
    out := errorChain{err: errs[0]}

    n := &out
    for _, err := range errs[1:] {
        n.next = &errorChain{err: err}
        n = n.next
    }
    return out
}
func (c errorChain) Is(err error) bool {
    return c.err == err
}

func (c errorChain) Unwrap() error {
    if c.next != nil {
        return c.next
    }
    return nil
}

https://go.dev/play/p/6oUGefSxhvF

有几种方法,但有一点您应该牢记:如果您有多个错误,您可能需要将其作为 slice 个错误来处理

例如,假设您需要检查所有错误是否相同,或者至少存在一个特定类型的错误,您可以使用下面的代码段。

您可以扩展这个概念或使用一些现有的库来处理多重错误

type Errors []error

func (errs Errors) String() string {
  …
}

func (errs Errors) Any(target error) bool{
    for _, err := range errs {
        if errors.Is(err,target) {
            return true
        }
    }
    return false
}

func (errs Errors) All(target error) bool{
    if len(errs) == 0 { return false }
    
    for _, err := range errs {
        if !errors.Is(err,target) {
            return false
        }
    }
    return true
}