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。
最终编辑:
感谢 和下面的评论,我现在有一个简单的方法来实现它(尽管,我仍然很好奇是否有一些标准类型可以做到这一点)。由于没有示例,我创建示例的尝试失败了。我缺少的部分是在我的类型中添加 Is
和 As
方法。因为自定义类型需要包含错误和指向下一个错误的指针,所以自定义 Is
和 As
方法允许我们比较自定义类型中包含的错误,而不是自定义类型本身。
这是一个工作示例: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.Is
和 errors.As
以递归方式处理自定义错误类型。很简单。
- 创建一个实现错误接口的类型(有一个
Error() string
方法)
- 该类型必须有一个代表包装错误的成员,该成员是指向其自身类型的指针。
- 实施
Unwrap() error
方法来 return 封装错误。
- 实现一些方法,将一个错误包装到另一个错误中
好像还不错。但是有麻烦了。
因为错误是指针,如果我做类似 myWrappedError = Wrap(Err1, Err2)
的事情(在这种情况下假设 Err1
被 Err2
包裹)。不仅 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 助手:Is
和 As
。 Is
允许您递归地展开错误,查找特定错误 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 错误,依此类推,这就是递归包装有价值的原因。
你可以这样使用:
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
}
有几种方法,但有一点您应该牢记:如果您有多个错误,您可能需要将其作为 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
}
在我看来,在 Go 中“包装”错误的“方法”是使用 fmt.Errof 和 %w 动词
https://go.dev/blog/go1.13-errors
但是,fmt.Errorf 不会递归地包装错误。无法使用它来包装三个先前定义的错误(Err1、Err2 和 Err3),然后使用 Is() 检查结果并为这三个错误中的每一个都获得 true。
最终编辑:
感谢 Is
和 As
方法。因为自定义类型需要包含错误和指向下一个错误的指针,所以自定义 Is
和 As
方法允许我们比较自定义类型中包含的错误,而不是自定义类型本身。
这是一个工作示例: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.Is
和 errors.As
以递归方式处理自定义错误类型。很简单。
- 创建一个实现错误接口的类型(有一个
Error() string
方法) - 该类型必须有一个代表包装错误的成员,该成员是指向其自身类型的指针。
- 实施
Unwrap() error
方法来 return 封装错误。 - 实现一些方法,将一个错误包装到另一个错误中
好像还不错。但是有麻烦了。
因为错误是指针,如果我做类似 myWrappedError = Wrap(Err1, Err2)
的事情(在这种情况下假设 Err1
被 Err2
包裹)。不仅 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 助手:Is
和 As
。 Is
允许您递归地展开错误,查找特定错误 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 错误,依此类推,这就是递归包装有价值的原因。
你可以这样使用:
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
}
有几种方法,但有一点您应该牢记:如果您有多个错误,您可能需要将其作为 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
}