errors: add AsType

Fixes #51945

Change-Id: Icda169782e796578eba728938134a85b5827d3b6
GitHub-Last-Rev: c6ff335ee1
GitHub-Pull-Request: golang/go#75621
Reviewed-on: https://go-review.googlesource.com/c/go/+/707235
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sean Liao <sean@liao.dev>
This commit is contained in:
Julien Cretel 2025-09-29 16:57:53 +00:00 committed by Damien Neil
parent 7c8166d02d
commit a846bb0aa5
6 changed files with 207 additions and 5 deletions

1
api/next/51945.txt Normal file
View file

@ -0,0 +1 @@
pkg errors, func AsType[$0 error](error) ($0, bool) #51945

View file

@ -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.

View file

@ -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)
// }
//

View file

@ -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)

View file

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

View file

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