diff --git a/api/next/51945.txt b/api/next/51945.txt new file mode 100644 index 00000000000000..7db1f093e5a2a5 --- /dev/null +++ b/api/next/51945.txt @@ -0,0 +1 @@ +pkg errors, func AsType[$0 error](error) ($0, bool) #51945 diff --git a/doc/next/6-stdlib/99-minor/errors/51945.md b/doc/next/6-stdlib/99-minor/errors/51945.md new file mode 100644 index 00000000000000..44ac7222e6d990 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/errors/51945.md @@ -0,0 +1,2 @@ +The new [AsType] function is a generic version of [As]. It is type-safe, faster, +and, in most cases, easier to use. diff --git a/src/errors/errors.go b/src/errors/errors.go index 5059be12ed441f..8b926cfe148c44 100644 --- a/src/errors/errors.go +++ b/src/errors/errors.go @@ -41,12 +41,12 @@ // // because the former will succeed if err wraps [io/fs.ErrExist]. // -// [As] examines the tree of its first argument looking for an error that can be -// assigned to its second argument, which must be a pointer. If it succeeds, it -// performs the assignment and returns true. Otherwise, it returns false. The form +// [AsType] examines the tree of its argument looking for an error whose +// type matches its type argument. If it succeeds, it returns the +// corresponding value of that type and true. Otherwise, it returns the +// zero value of that type and false. The form // -// var perr *fs.PathError -// if errors.As(err, &perr) { +// if perr, ok := errors.AsType[*fs.PathError](err); ok { // fmt.Println(perr.Path) // } // diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 278df8c7da6e5a..92ef36b1010edb 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -102,6 +102,18 @@ func ExampleAs() { // Failed at path: non-existing } +func ExampleAsType() { + if _, err := os.Open("non-existing"); err != nil { + if pathError, ok := errors.AsType[*fs.PathError](err); ok { + fmt.Println("Failed at path:", pathError.Path) + } else { + fmt.Println(err) + } + } + // Output: + // Failed at path: non-existing +} + func ExampleUnwrap() { err1 := errors.New("error1") err2 := fmt.Errorf("error2: [%w]", err1) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index eec9591dae7b93..2ebb951f1de93d 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -80,6 +80,10 @@ func is(err, target error, targetComparable bool) bool { // As finds the first error in err's tree that matches target, and if one is found, sets // target to that error value and returns true. Otherwise, it returns false. // +// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its target +// argument rather than returning the matching error and doesn't require its target +// argument to implement error. +// // 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, As examines err followed by a depth-first traversal of its children. @@ -145,3 +149,60 @@ func as(err error, target any, targetVal reflectlite.Value, targetType reflectli } var errorType = reflectlite.TypeOf((*error)(nil)).Elem() + +// AsType finds the first error in err's tree that matches the type E, and +// if one is found, returns that error value and true. Otherwise, it +// returns the zero value of E and false. +// +// 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, AsType examines err followed by a +// depth-first traversal of its children. +// +// An error err matches the type E if the type assertion err.(E) holds, +// or if the error has a method As(any) bool such that err.As(target) +// returns true when target is a non-nil *E. In the latter case, the As +// method is responsible for setting target. +func AsType[E error](err error) (E, bool) { + if err == nil { + var zero E + return zero, false + } + var pe *E // lazily initialized + return asType(err, &pe) +} + +func asType[E error](err error, ppe **E) (_ E, _ bool) { + for { + if e, ok := err.(E); ok { + return e, true + } + if x, ok := err.(interface{ As(any) bool }); ok { + if *ppe == nil { + *ppe = new(E) + } + if x.As(*ppe) { + return **ppe, true + } + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if err == nil { + continue + } + if x, ok := asType(err, ppe); ok { + return x, true + } + } + return + default: + return + } + } +} diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 58ed95fd9a0fc7..81c795a6bb8b18 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -239,6 +239,123 @@ func TestAsValidation(t *testing.T) { } } +func TestAsType(t *testing.T) { + var errT errorT + var errP *fs.PathError + type timeout interface { + Timeout() bool + error + } + _, errF := os.Open("non-existing") + poserErr := &poser{"oh no", nil} + + testAsType(t, + nil, + errP, + false, + ) + testAsType(t, + wrapped{"pitied the fool", errorT{"T"}}, + errorT{"T"}, + true, + ) + testAsType(t, + errF, + errF, + true, + ) + testAsType(t, + errT, + errP, + false, + ) + testAsType(t, + wrapped{"wrapped", nil}, + errT, + false, + ) + testAsType(t, + &poser{"error", nil}, + errorT{"poser"}, + true, + ) + testAsType(t, + &poser{"path", nil}, + poserPathErr, + true, + ) + testAsType(t, + poserErr, + poserErr, + true, + ) + testAsType(t, + errors.New("err"), + timeout(nil), + false, + ) + testAsType(t, + errF, + errF.(timeout), + true) + testAsType(t, + wrapped{"path error", errF}, + errF.(timeout), + true, + ) + testAsType(t, + multiErr{}, + errT, + false, + ) + testAsType(t, + multiErr{errors.New("a"), errorT{"T"}}, + errorT{"T"}, + true, + ) + testAsType(t, + multiErr{errorT{"T"}, errors.New("a")}, + errorT{"T"}, + true, + ) + testAsType(t, + multiErr{errorT{"a"}, errorT{"b"}}, + errorT{"a"}, + true, + ) + testAsType(t, + multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}, + errorT{"a"}, + true, + ) + testAsType(t, + multiErr{wrapped{"path error", errF}}, + errF.(timeout), + true, + ) + testAsType(t, + multiErr{nil}, + errT, + false, + ) +} + +type compError interface { + comparable + error +} + +func testAsType[E compError](t *testing.T, err error, want E, wantOK bool) { + t.Helper() + name := fmt.Sprintf("AsType[%T](Errorf(..., %v))", want, err) + t.Run(name, func(t *testing.T) { + got, gotOK := errors.AsType[E](err) + if gotOK != wantOK || got != want { + t.Fatalf("got %v, %t; want %v, %t", got, gotOK, want, wantOK) + } + }) +} + func BenchmarkIs(b *testing.B) { err1 := errors.New("1") err2 := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} @@ -260,6 +377,15 @@ func BenchmarkAs(b *testing.B) { } } +func BenchmarkAsType(b *testing.B) { + err := multiErr{multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}} + for range b.N { + if _, ok := errors.AsType[errorT](err); !ok { + b.Fatal("AsType failed") + } + } +} + func TestUnwrap(t *testing.T) { err1 := errors.New("1") erra := wrapped{"wrap 2", err1}