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

huangapple go评论79阅读模式
英文:

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.Errorf%w动词来包装错误。但是,fmt.Errorf并不递归地包装错误。如果你想要包装之前定义的三个错误(Err1、Err2和Err3),并使用Is()方法检查结果,无法使用fmt.Errorf实现这一点。

根据你提供的代码,你可以自定义一个错误类型,并实现Is()As()方法来递归地包装错误。下面是一个工作示例:

package main

import (
	"errors"
	"fmt"
)

type errorChain struct {
	err  error
	next *errorChain
}

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

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

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

这个示例中,我们定义了一个errorChain结构体,它包含一个错误和指向下一个错误的指针。我们还实现了Is()As()方法来递归地比较错误。Wrap()函数用于包装错误,并返回一个包含错误链的errorChain实例。

希望这可以帮助到你!如果你有任何其他问题,请随时问我。

英文:

Everywhere I look, the "way" to "wrap" errors in Go is to use fmt.Errof with the %w verb

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

However, fmt.Errorf does not recursively wrap errors. There is no way to use it to wrap three previously defined errors (Err1, Err2, and Err3) and then check the result by using Is() and get true for each those three errors.

FINAL EDIT:

Thanks to @mkopriva's answer and comments below it, I now have a straightforward way to implement this (although, I am still curious if there is some standard type which does this). In the absence of an example, my attempts at creating one failed. The piece I was missing was adding an Is and As method to my type. Because the custom type needs to contain an error and a pointer to the next error, the custom Is and As methods allows us to compare the error contained in the custom type, rather than the custom type itself.

Here is a working example: https://go.dev/play/p/6BYGgIb728k

Highlights from the above 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
}

While the Go source specifically mentions the ability to define an Is method, the example does not implement it in a way that can solve my issue and the discussion do not make it immediately clear that it would be needed to utilize the recursive nature of errors.Is.

AND NOW BACK TO THE ORIGINAL POST:

Is there something built into Go where this does work?

I played around with making one of my own (several attempts), but ran into undesirable issues. These issues stem from the fact that errors in Go appear to be compared by address. i.e. if Err1 and Err2 point to the same thing, they are the same.

This causes me issues. I can naively get errors.Is and errors.As to work recursively with a custom error type. It is straightforward.

  1. Make a type that implements the error interface (has an Error() string method)
  2. The type must have a member that represents the wrapped error which is a pointer to its own type.
  3. Implement an Unwrap() error method that returns the wrapped error.
  4. Implement some method which wraps one error with another

It seems good. But there is trouble.

Since errors are pointers, if I make something like myWrappedError = Wrap(Err1, Err2) (in this case assume Err1 is being wrapped by Err2). Not only will errors.Is(myWrappedError, Err1) and errors.Is(myWrappedError, Err2) return true, but so will errors.Is(Err2, Err1)

Should the need arise to make myOtherWrappedError = Wrap(Err3, Err2) and later call errors.Is(myWrappedError, Err1) it will now return false! Making myOtherWrappedError changes myWrappedError.

I tried several approaches, but always ran into related issues.

Is this possible? Is there a Go library which does this?

NOTE: I am more interested in the presumably already existing right way to do this rather than the specific thing that is wrong with my basic attempt

Edit 3: As suggested by one of the answers, the issue in my first code is obviously that I modify global errors. I am aware, but failed to adequately communicate. Below, I will include other broken code which uses no pointers and modifies no globals.

Edit 4: slight modification to make it work more, but it is still broken

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

Edit 2: playground version shortened

See https://go.dev/play/p/swFPajbMcXA for an example of this.

EDIT 1: A trimmed version of my code focusing on the important parts:

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
}

答案1

得分: 2

你可以使用类似这样的代码:

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

英文:

You can use something like this:

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

答案2

得分: 1

你的代码修改了全局错误值,因此本质上是有问题的。这个缺陷与Go的错误处理机制无关。

根据你提供的文档,有两个错误处理的辅助函数:IsAsIs 允许你递归地解包错误,查找特定的错误 ,这个值必须是一个对于实用性来说必须是一个全局包变量。另一方面,As 允许你递归地解包错误,查找给定 类型 的任何包装错误值。

包装是如何工作的?你将错误 A 包装在一个新的错误值 B 中。Wrap() 辅助函数应该返回一个新值,就像链接文档中的 fmt.Errorf 示例一样。Wrap 辅助函数不应该修改被包装的错误值。该值应该被视为不可变的。实际上,在任何正常的实现中,该值应该是 error 类型的,这样你就可以包装任何错误,而不仅仅是将你的自定义错误类型的同心值包装在一起;而且,在这种情况下,你无法访问被包装错误的字段来修改它们。基本上,Wrap 应该大致是这样的:

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

就是这样。这并不是非常有用,因为你的 errGroup 实现实际上并没有做任何事情——它不提供关于发生的错误的任何细节,它只是其他错误的容器。为了使其有价值,它应该有一个 string 类型的错误消息,或者像其他一些错误类型的 IsNotFound 方法一样,或者其他使其比仅使用 errorfmt.Errorf 更有用的方法。

根据你示例代码中的用法,看起来你假设使用情况是说“我想将 A 包装在 B 中,在 C 中包装”,我从未在实际中见过这种情况,也想不出任何需要这样做的场景。包装的目的是说“我收到了错误 A,我要将其包装在错误 B 中以添加上下文,并返回它”。调用者可能会将错误包装在错误 C 中,依此类推,这就是递归包装的价值所在。

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

英文:

Your code modifies package-global error values, so it is inherently broken. This defect has nothing to do with Go's error handling mechanics.

Per the documentation you linked, there are two error-handling helpers: Is, and As. Is lets you recursively unwrap an error, looking for a specific error value, which is necessarily a package global for this to be useful. As, on the other hand, lets you recursively unwrap an error looking for any wrapped error value of a given type.

How does wrapping work? You wrap error A in a new error value B. A Wrap() helper would necessarily return a new value, as fmt.Errorf does in the examples in the linked documentation. A Wrap helper should never modify the value of the error being wrapped. That value should be considered immutable. In fact, in any normal implementation, the value would be of type error, so that you can wrap any error, rather than just wrapping concentric values of your custom error type in each other; and, in that case, you have no access to the fields of the wrapped error to modify them anyway. Essentially, Wrap should be roughly:

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

And that's it. That's not very useful, because your implementation of errGroup doesn't really do anything - it provides no details about the error that occurred, it's just a container for other errors. For it to have value, it should have a string error message, or methods like some other error types' IsNotFound, or something that makes it more useful than just using error and fmt.Errorf.

Based on the usage in your example code, it also looks like you're presuming the use case is to say "I want to wrap A in B in C", which I've never seen in the wild and I cannot think of any scenario where that would be needed. The purpose of wrapping is to say "I've recieved error A, I'm going to wrap it in error B to add context, and return it". The caller might wrap that error in error C, and so on, which is what makes recursive wrapping valuable.

For example: https://go.dev/play/p/XeoONx19dgX

答案3

得分: 1

你将能够返回一个错误切片/树,而不是使用链式包装。这个功能将在 Go 1.20 中实现(预计在 2022 年 12 月发布的 Go 1.20-rc1 中)。在此期间,mdobak/go-xerrors 是一个很好的替代方案。

发布说明1解释了这个功能的细节:

包装多个错误

Go 1.20 扩展了对错误包装的支持,允许一个错误包装多个其他错误。

通过提供一个返回 []errorUnwrap 方法,错误 e 可以包装多个错误。

errors.Iserrors.As 函数已更新以检查多个包装的错误。

fmt.Errorf 函数现在支持多个 %w 格式动词的出现,这将导致它返回一个包装了所有这些错误操作数的错误。

新的函数 errors.Join 返回一个包装了错误列表的错误。

这个功能的提案可以在这里找到:

背景

自从 Go 1.13 以来,一个错误可以通过提供一个返回被包装错误的 Unwrap 方法来包装另一个错误。
errors.Iserrors.As 函数可以操作包装错误的链。

一个常见的需求是将一系列错误组合成一个单一的错误。

提案

如果一个错误的类型具有方法

Unwrap() []error

那么该错误将包装多个错误。

重用 Unwrap 这个名称可以避免与现有的单数 Unwrap 方法产生歧义。
Unwrap 返回一个长度为 0 的列表意味着该错误没有包装任何内容。

调用者不能修改 Unwrap 返回的列表。
Unwrap 返回的列表不能包含任何 nil 错误。

我们将术语“错误链”替换为“错误树”。

errors.Iserrors.As 函数已更新以解包多个错误。

  • Is 如果树中的任何错误匹配,则报告匹配。
  • As 在树的中序遍历中找到第一个匹配的错误。

errors.Join 函数提供了一个简单的 multierr 实现,它不会展开错误。

// Join 返回一个包装了给定错误的错误。
// 任何空错误值都会被丢弃。
// 错误的格式为给定错误的文本,用换行符分隔。
// 如果 errs 不包含非空值,则返回 nil。
func Join(errs ...error) error

fmt.Errorf 函数允许多个 %w 格式动词的出现。

errors.Unwrap 函数不受影响:当在具有 Unwrap() []error 方法的错误上调用时,它返回 nil

为什么应该将此功能加入标准库?

这个提案添加了标准库之外无法提供的功能:对 errors.Iserrors.As 中的错误树的直接支持。

现有的组合错误通过提供 IsAs 方法来操作包含的错误,要求每个实现在可能不兼容的方式上重复这个逻辑。出于与处理单数解包的原因相同,这最好在 errors.Iserrors.As 中处理。

此外,该提案为生态系统提供了一种常用的方法来表示组合错误,允许第三方实现之间的互操作。

到目前为止(2022 年 9 月),这个提案已经被接受了!

CL 432575 开始了实现过程。

英文:

Instead of chaining/wrapping, you will "soon" (Go 1.20, as seen in Go 1.20-rc1 in Dec. 2022) be able to return a slice/tree of errors.
(In the meantime, mdobak/go-xerrors is a good alternative)


The release note explains:

> ## Wrapping multiple errors
>
> Go 1.20 expands support for error wrapping to permit an error to wrap
> multiple other errors.
>
> An error e can wrap more than one error by providing an Unwrap method
> that returns a []error.
>
> The errors.Is and errors.As functions have been updated to inspect
> multiply wrapped errors.
>
> The fmt.Errorf function now supports multiple occurrences of the %w
> format verb, which will cause it to return an error that wraps all of
> those error operands.
>
> The new function errors.Join returns an error wrapping a list of
> errors.


That comes from:

proposal: errors: add support for wrapping multiple errors

> ## Background
>
> Since Go 1.13, an error may wrap another by providing an Unwrap method returning the wrapped error.
The errors.Is and errors.As functions operate on chains of wrapped errors.
>
> A common request is for a way to combine a list of errors into a single error.
>
> ## Proposal
>
> An error wraps multiple errors if its type has the method
>
> Unwrap() []error
>
> Reusing the name Unwrap avoids ambiguity with the existing singular Unwrap method.
Returning a 0-length list from Unwrap means the error doesn't wrap anything.
>
> Callers must not modify the list returned by Unwrap.
The list returned by Unwrap must not contain any nil errors.
>
> We replace the term "error chain" with "error tree".
>
> The errors.Is and errors.As functions are updated to unwrap multiple errors.
>
> - Is reports a match if any error in the tree matches.
> - As finds the first matching error in a inorder preorder traversal of the tree.
>
> The errors.Join function provides a simple implementation of a multierr.
It does not flatten errors.
>
> // Join returns an error that wraps the given errors.
> // Any nil error values are discarded.
> // The error formats as the text of the given errors, separated by newlines.
> // Join returns nil if errs contains no non-nil values.
> func Join(errs ...error) error
>
> The fmt.Errorf function permits multiple instances of the %w formatting verb.
>
> The errors.Unwrap function is unaffected: It returns nil when called on an error with an Unwrap() []error method.
>
> ## Why should this be in the standard library?
>
> This proposal adds something which cannot be provided outside the standard library: Direct support for error trees in errors.Is and errors.As.
>
> Existing combining errors operate by providing Is and As methods which inspect the contained errors, requiring each implementation to duplicate this logic, possibly in incompatible ways.
This is best handled in errors.Is and errors.As, for the same reason those functions handle singular unwrapping.
>
> In addition, this proposal provides a common method for the ecosystem to use to represent combined errors, permitting interoperation between third-party implementations.

So far (Sept. 2022) this proposal <del>seems a likely accept</del> has been accepted!

CL 432575 starts the implementation.

答案4

得分: 0

有几种方法可以处理多个错误,但有一件事情你需要记住:如果你有多个错误,你可能需要将其作为错误的切片来处理。

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

你可以扩展这个概念,或者使用一些现有的库来处理多个错误。

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
}
英文:

There arr several approaches but there is one thing that you should keep in mind: if you have multiple errors, you may need to handle it as a slice of errors

For instance, imagine you need to check if all errors are the same, or there is at least one error of certain type you can use the snippet below.

You can extend this concept or use some existing library to handle multierrors

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
}

</details>



huangapple
  • 本文由 发表于 2022年3月29日 01:16:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/71651182.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定