From 51fd07f31737a9aa162f18a1ec3a46ea80ba2f29 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:07:01 +0400 Subject: [PATCH 1/4] add errors Match and IsAny Add Match and IsAny, but functions are slow. --- src/errors/example_test.go | 54 +++++++++ src/errors/wrap.go | 117 ++++++++++++++++++++ src/errors/wrap_test.go | 219 +++++++++++++++++++++++++++++++++++++ 3 files changed, 390 insertions(+) diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 92ef36b1010edb..fe20214bb4294d 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -123,3 +123,57 @@ func ExampleUnwrap() { // error2: [error1] // error1 } + +func ExampleIsAny() { + var ( + ErrNotFound = errors.New("not found") + ErrPermission = errors.New("permission denied") + ErrTimeout = errors.New("timeout") + ) + + // Simulate receiving an error + err := fmt.Errorf("database query failed: %w", ErrTimeout) + + // Check if the error matches any of the known errors + if errors.IsAny(err, ErrNotFound, ErrPermission, ErrTimeout) { + fmt.Println("error is one of the expected types") + } + + // Check against a different set + if !errors.IsAny(err, ErrNotFound, ErrPermission) { + fmt.Println("error is not a not-found or permission error") + } + + // Output: + // error is one of the expected types + // error is not a not-found or permission error +} + +func ExampleMatch() { + var ( + ErrNotFound = errors.New("not found") + ErrNetworkIssue = errors.New("network issue") + ErrDiskFull = errors.New("disk full") + ) + + err := fmt.Errorf("operation failed: %w", ErrNetworkIssue) + + // Match returns the matched error from the targets + if matched := errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull); matched != nil { + fmt.Println("matched error:", matched) + } + + // Can be used in a switch statement + switch errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull) { + case ErrNotFound: + fmt.Println("resource not found") + case ErrNetworkIssue: + fmt.Println("network issue detected") + case ErrDiskFull: + fmt.Println("disk is full") + } + + // Output: + // matched error: network issue + // network issue detected +} diff --git a/src/errors/wrap.go b/src/errors/wrap.go index 2ebb951f1de93d..04c37ce1f77861 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -206,3 +206,120 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { } } } + +func IsAnySlow(err error, targets ...error) bool { + if err == nil { + for _, target := range targets { + if target == nil { + return true + } + } + return false + } + if len(targets) == 0 { + return false + } + + for _, target := range targets { + if Is(err, target) { + return true + } + } + return false +} + +// IsAny reports whether any error in err's tree matches any of the target errors. +// +// The tree consists of err itself, followed by the errors obtained by repeatedly +// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple +// errors, IsAny examines err followed by a depth-first traversal of its children. +// +// IsAny returns true if [Is](err, target) returns true for any target in targets. +func IsAny(err error, targets ...error) bool { + _, found := match(err, targets) + + return found +} + +// Match returns the matched error in targets for err. +// +// The tree consists of err itself, followed by the errors obtained by repeatedly +// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple +// errors, Match examines err followed by a depth-first traversal of its children. +// +// Match returns the first target error for which [Is](err, target) returns true. +// If no target matches, Match returns nil. +func Match(err error, targets ...error) error { + matched, _ := match(err, targets) + + return matched +} + +func match(err error, targets []error) (error, bool) { + if err == nil { + for _, target := range targets { + if target == nil { + return nil, true + } + } + return nil, false + } + + if len(targets) == 0 { + return nil, false + } else if len(targets) == 1 { + if Is(err, targets[0]) { + return targets[0], true + } + + return nil, false + } + + targetMap := make(map[error]struct{}, len(targets)) + for _, target := range targets { + if target != nil && reflectlite.TypeOf(target).Comparable() { + targetMap[target] = struct{}{} + } + } + + isErrComparable := reflectlite.TypeOf(err).Comparable() + for { + if isErrComparable && len(targetMap) > 0 { + if _, ok := targetMap[err]; ok { + for _, target := range targets { + if target == err { + return target, true + } + } + } + } + + if x, ok := err.(interface{ Is(error) bool }); ok { + for _, target := range targets { + if target != nil && x.Is(target) { + return target, true + } + } + } + + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return nil, false + } + isErrComparable = reflectlite.TypeOf(err).Comparable() + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if err != nil { + if matched, found := match(err, targets); matched != nil { + return matched, found + } + } + } + return nil, false + default: + return nil, false + } + } +} diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 81c795a6bb8b18..3a505efcdf9f41 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -436,3 +436,222 @@ func (errorUncomparable) Is(target error) bool { _, ok := target.(errorUncomparable) return ok } + +func TestIsAny(t *testing.T) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + erra := wrapped{"wrap a", err1} + errb := wrapped{"wrap b", err2} + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + targets []error + match bool + }{ + // Basic cases + {nil, []error{nil}, true}, + {nil, []error{err1}, false}, + {err1, []error{nil}, false}, + {err1, []error{err1}, true}, + {err1, []error{err2}, false}, + {err1, []error{err1, err2}, true}, + {err1, []error{err2, err1}, true}, + {err1, []error{err2, err3}, false}, + + // Wrapped errors + {erra, []error{err1}, true}, + {erra, []error{err2}, false}, + {erra, []error{err1, err2}, true}, + {erra, []error{err2, err1}, true}, + {erra, []error{err2, err3}, false}, + + // Multiple targets with wrapped errors + {errb, []error{err1, err2, err3}, true}, + {errb, []error{err1, err3}, false}, + + // Posers + {poser, []error{err1}, true}, + {poser, []error{err3}, true}, + {poser, []error{err2}, false}, + {poser, []error{err1, err2}, true}, + {poser, []error{err2, err3}, true}, + {poser, []error{err2, erra}, false}, + + // Multi errors + {multiErr{}, []error{err1}, false}, + {multiErr{err1, err2}, []error{err1}, true}, + {multiErr{err1, err2}, []error{err2}, true}, + {multiErr{err1, err2}, []error{err3}, false}, + {multiErr{err1, err2}, []error{err3, err1}, true}, + {multiErr{err1, err2}, []error{err3, erra}, false}, + {multiErr{erra, errb}, []error{err1, err2}, true}, + {multiErr{erra, errb}, []error{err3, err1}, true}, + + // Empty targets + {err1, []error{}, false}, + {nil, []error{}, false}, + + // Uncomparable errors + {errorUncomparable{}, []error{errorUncomparable{}}, true}, + {&errorUncomparable{}, []error{errorUncomparable{}}, true}, + {errorUncomparable{}, []error{err1, errorUncomparable{}}, true}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + if got := errors.IsAny(tc.err, tc.targets...); got != tc.match { + t.Errorf("IsAny(%v, %v) = %v, want %v", tc.err, tc.targets, got, tc.match) + } + }) + } +} + +func TestMatch(t *testing.T) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + erra := wrapped{"wrap a", err1} + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + targets []error + want error // the expected matched error + }{ + {err1, []error{err1}, err1}, + {err1, []error{err2}, nil}, + {err1, []error{err1, err2}, err1}, + {err1, []error{err2, err1}, err1}, // Returns first match (err1) + {err1, []error{err2, err3}, nil}, + {erra, []error{err1, err2}, err1}, + {erra, []error{err2, err1}, err1}, // erra wraps err1, so matches err1 + {erra, []error{err2, err3}, nil}, + {nil, []error{nil}, nil}, + {nil, []error{err1}, nil}, + {err1, []error{}, nil}, + + // Posers - note that the poser matches err1 or err3 + {poser, []error{err1}, err1}, + {poser, []error{err3}, err3}, + {poser, []error{err2}, nil}, + {poser, []error{err2, err1}, err1}, + {poser, []error{err1, err3}, err1}, // Returns first match + + // Multi errors + {multiErr{err1, err2}, []error{err1}, err1}, + {multiErr{err1, err2}, []error{err2}, err2}, + {multiErr{err1, err2}, []error{err3}, nil}, + {multiErr{err1, err2}, []error{err3, err2}, err2}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + got := errors.Match(tc.err, tc.targets...) + if got != tc.want { + t.Errorf("Match(%v, %v) = %v, want %v", tc.err, tc.targets, got, tc.want) + } + }) + } +} + +// TODO remove +func BenchmarkIsAnySlow(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAnySlow(err, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAnySlow(err, err2, err3, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.IsAnySlow(err, err2, err3) { + b.Fatal("IsAny should not match") + } + } + }) +} + +func BenchmarkIsAny(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAny(err, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAny(err, err2, err3, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.IsAny(err, err2, err3) { + b.Fatal("IsAny should not match") + } + } + }) +} + +func BenchmarkMatch(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err1) != err1 { + b.Fatal("Match failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err2, err3, err1) != err1 { + b.Fatal("Match failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err2, err3) != nil { + b.Fatal("Match should not match") + } + } + }) +} From 0fa8635c1eef1ef53ce61960109ed93f84081814 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:33:46 +0400 Subject: [PATCH 2/4] add errors Match and IsAny improved performance of Match and IsAny --- src/errors/wrap.go | 33 +++------------- src/errors/wrap_test.go | 87 +++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 73 deletions(-) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index 04c37ce1f77861..ba1dd9f50253c7 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -207,27 +207,6 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { } } -func IsAnySlow(err error, targets ...error) bool { - if err == nil { - for _, target := range targets { - if target == nil { - return true - } - } - return false - } - if len(targets) == 0 { - return false - } - - for _, target := range targets { - if Is(err, target) { - return true - } - } - return false -} - // IsAny reports whether any error in err's tree matches any of the target errors. // // The tree consists of err itself, followed by the errors obtained by repeatedly @@ -282,15 +261,15 @@ func match(err error, targets []error) (error, bool) { } } + return matching(err, targets, targetMap) +} + +func matching(err error, targets []error, targetMap map[error]struct{}) (error, bool) { isErrComparable := reflectlite.TypeOf(err).Comparable() for { if isErrComparable && len(targetMap) > 0 { if _, ok := targetMap[err]; ok { - for _, target := range targets { - if target == err { - return target, true - } - } + return err, true } } @@ -312,7 +291,7 @@ func match(err error, targets []error) (error, bool) { case interface{ Unwrap() []error }: for _, err := range x.Unwrap() { if err != nil { - if matched, found := match(err, targets); matched != nil { + if matched, found := matching(err, targets, targetMap); matched != nil { return matched, found } } diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 3a505efcdf9f41..22da8f0b3ca34e 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -562,36 +562,15 @@ func TestMatch(t *testing.T) { } } -// TODO remove -func BenchmarkIsAnySlow(b *testing.B) { - err1 := errors.New("1") - err2 := errors.New("2") - err3 := errors.New("3") - err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} - - b.Run("one_target", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAnySlow(err, err1) { - b.Fatal("IsAny failed") - } +// isAnySlow is a naive implementation of IsAny for benchmarking purposes. +func isAnySlow(err error, targets ...error) bool { + for _, target := range targets { + if errors.Is(err, target) { + return true } - }) - - b.Run("three_targets", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAnySlow(err, err2, err3, err1) { - b.Fatal("IsAny failed") - } - } - }) + } - b.Run("no_match", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if errors.IsAnySlow(err, err2, err3) { - b.Fatal("IsAny should not match") - } - } - }) + return false } func BenchmarkIsAny(b *testing.B) { @@ -600,29 +579,45 @@ func BenchmarkIsAny(b *testing.B) { err3 := errors.New("3") err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} - b.Run("one_target", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAny(err, err1) { - b.Fatal("IsAny failed") + testCases := []struct { + name string + fn func(error, ...error) bool + }{ + { + name: "IsAny", + fn: errors.IsAny, + }, + { + name: "isAnySlow", + fn: isAnySlow, + }, + } + + for _, tc := range testCases { + b.Run(tc.name+"_one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !tc.fn(err, err1) { + b.Fatal(tc.name, "failed") + } } - } - }) + }) - b.Run("three_targets", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAny(err, err2, err3, err1) { - b.Fatal("IsAny failed") + b.Run(tc.name+"three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !tc.fn(err, err2, err3, err1) { + b.Fatal(tc.name, "failed") + } } - } - }) + }) - b.Run("no_match", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if errors.IsAny(err, err2, err3) { - b.Fatal("IsAny should not match") + b.Run(tc.name+"no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if tc.fn(err, err2, err3) { + b.Fatal(tc.name, "should not match") + } } - } - }) + }) + } } func BenchmarkMatch(b *testing.B) { From b3d0973da1d98dd7e3d8226d9b62577806cad734 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:44:28 +0400 Subject: [PATCH 3/4] add errors Match and IsAny Simplified documentation --- src/errors/wrap.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index ba1dd9f50253c7..87e82600b4873e 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -212,22 +212,21 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { // The tree consists of err itself, followed by the errors obtained by repeatedly // calling its Unwrap() error or Unwrap() []error method. When err wraps multiple // errors, IsAny examines err followed by a depth-first traversal of its children. -// -// IsAny returns true if [Is](err, target) returns true for any target in targets. func IsAny(err error, targets ...error) bool { _, found := match(err, targets) return found } -// Match returns the matched error in targets for err. +// Match returns the first target error from targets that matches any error in err's tree. // // The tree consists of err itself, followed by the errors obtained by repeatedly // calling its Unwrap() error or Unwrap() []error method. When err wraps multiple // errors, Match examines err followed by a depth-first traversal of its children. // -// Match returns the first target error for which [Is](err, target) returns true. -// If no target matches, Match returns nil. +// Match returns the first target from targets if an err is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// If no target matches the err, Match returns nil. func Match(err error, targets ...error) error { matched, _ := match(err, targets) From 36dafdbbd04d5c8757d14cb737ee9e9759505169 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sun, 5 Oct 2025 00:00:17 +0400 Subject: [PATCH 4/4] add errors Match and IsAny Simplified examples --- src/errors/example_test.go | 59 ++++++++++++-------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/errors/example_test.go b/src/errors/example_test.go index fe20214bb4294d..77193a05b95bcf 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -125,55 +125,34 @@ func ExampleUnwrap() { } func ExampleIsAny() { - var ( - ErrNotFound = errors.New("not found") - ErrPermission = errors.New("permission denied") - ErrTimeout = errors.New("timeout") - ) - - // Simulate receiving an error - err := fmt.Errorf("database query failed: %w", ErrTimeout) - - // Check if the error matches any of the known errors - if errors.IsAny(err, ErrNotFound, ErrPermission, ErrTimeout) { - fmt.Println("error is one of the expected types") - } - - // Check against a different set - if !errors.IsAny(err, ErrNotFound, ErrPermission) { - fmt.Println("error is not a not-found or permission error") + if _, err := os.Open("non-existing"); err != nil { + if errors.IsAny(err, fs.ErrNotExist, fs.ErrInvalid) { + fmt.Println("file does not exist") + } else { + fmt.Println(err) + } } - // Output: - // error is one of the expected types - // error is not a not-found or permission error + // file does not exist } func ExampleMatch() { - var ( - ErrNotFound = errors.New("not found") - ErrNetworkIssue = errors.New("network issue") - ErrDiskFull = errors.New("disk full") - ) + _, err := os.Open("non-existing") - err := fmt.Errorf("operation failed: %w", ErrNetworkIssue) - - // Match returns the matched error from the targets - if matched := errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull); matched != nil { + matched := errors.Match(err, fs.ErrNotExist, fs.ErrInvalid) + if matched != nil { fmt.Println("matched error:", matched) + } else { + fmt.Println("no match") } - // Can be used in a switch statement - switch errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull) { - case ErrNotFound: - fmt.Println("resource not found") - case ErrNetworkIssue: - fmt.Println("network issue detected") - case ErrDiskFull: - fmt.Println("disk is full") + switch matched { + case fs.ErrNotExist: + fmt.Println("file does not exist") + case fs.ErrInvalid: + fmt.Println("invalid argument") } - // Output: - // matched error: network issue - // network issue detected + // matched error: file does not exist + // file does not exist }