diff options
author | Marcel van Lohuizen <mpvl@golang.org> | 2019-02-23 00:09:40 +0100 |
---|---|---|
committer | Marcel van Lohuizen <mpvl@golang.org> | 2019-02-27 19:09:40 +0000 |
commit | 62f5e8156ef56fa61e6af56f4ccc633bde1a9120 (patch) | |
tree | ebec87666eca8e47194fdc2a7431c5a13f4ac3cd /src/errors | |
parent | 37f84817247d3b8e687a701ccb0d6bc7ffe3cb78 (diff) | |
download | go-62f5e8156ef56fa61e6af56f4ccc633bde1a9120.tar.gz go-62f5e8156ef56fa61e6af56f4ccc633bde1a9120.zip |
errors: add Unwrap, Is, and As
Unwrap, Is and As are as defined in proposal
Issue #29934.
Also add Opaque for enforcing an error cannot
be unwrapped.
Change-Id: I4f3feaa42e3ee7477b588164ac622ba4d5e77cad
Reviewed-on: https://go-review.googlesource.com/c/163558
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
Diffstat (limited to 'src/errors')
-rw-r--r-- | src/errors/example_test.go | 15 | ||||
-rw-r--r-- | src/errors/wrap.go | 106 | ||||
-rw-r--r-- | src/errors/wrap_test.go | 258 |
3 files changed, 379 insertions, 0 deletions
diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 5dc8841237..7724c16cdf 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -5,7 +5,9 @@ package errors_test import ( + "errors" "fmt" + "os" "time" ) @@ -32,3 +34,16 @@ func Example() { } // Output: 1989-03-15 22:30:00 +0000 UTC: the file system has gone away } + +func ExampleAs() { + _, err := os.Open("non-existing") + if err != nil { + var pathError *os.PathError + if errors.As(err, &pathError) { + fmt.Println("Failed at path:", pathError.Path) + } + } + + // Output: + // Failed at path: non-existing +} diff --git a/src/errors/wrap.go b/src/errors/wrap.go new file mode 100644 index 0000000000..fc7bf71f8a --- /dev/null +++ b/src/errors/wrap.go @@ -0,0 +1,106 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package errors + +import ( + "internal/reflectlite" +) + +// A Wrapper provides context around another error. +type Wrapper interface { + // Unwrap returns the next error in the error chain. + // If there is no next error, Unwrap returns nil. + Unwrap() error +} + +// Opaque returns an error with the same error formatting as err +// but that does not match err and cannot be unwrapped. +func Opaque(err error) error { + return noWrapper{err} +} + +type noWrapper struct { + error +} + +func (e noWrapper) FormatError(p Printer) (next error) { + if f, ok := e.error.(Formatter); ok { + return f.FormatError(p) + } + p.Print(e.error) + return nil +} + +// Unwrap returns the result of calling the Unwrap method on err, if err +// implements Unwrap. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + u, ok := err.(Wrapper) + if !ok { + return nil + } + return u.Unwrap() +} + +// Is reports whether any error in err's chain matches target. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { + if target == nil { + return err == target + } + for { + if err == target { + return true + } + if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { + return true + } + // TODO: consider supporing target.Is(err). This would allow + // user-definable predicates, but also may allow for coping with sloppy + // APIs, thereby making it easier to get away with them. + if err = Unwrap(err); err == nil { + return false + } + } +} + +// As finds the first error in err's chain that matches the type to which target +// points, and if so, sets the target to its value and returns true. An error +// matches a type if it is assignable to the target type, or if it has a method +// As(interface{}) bool such that As(target) returns true. As will panic if +// target is not a non-nil pointer to a type which implements error or is of +// interface type. +// +// The As method should set the target to its value and return true if err +// matches the type to which target points. +func As(err error, target interface{}) bool { + if target == nil { + panic("errors: target cannot be nil") + } + val := reflectlite.ValueOf(target) + typ := val.Type() + if typ.Kind() != reflectlite.Ptr || val.IsNil() { + panic("errors: target must be a non-nil pointer") + } + if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) { + panic("errors: *target must be interface or implement error") + } + targetType := typ.Elem() + for { + if reflectlite.TypeOf(err).AssignableTo(targetType) { + val.Elem().Set(reflectlite.ValueOf(err)) + return true + } + if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) { + return true + } + if err = Unwrap(err); err == nil { + return false + } + } +} + +var errorType = reflectlite.TypeOf((*error)(nil)).Elem() diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go new file mode 100644 index 0000000000..657890c1a6 --- /dev/null +++ b/src/errors/wrap_test.go @@ -0,0 +1,258 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package errors_test + +import ( + "bytes" + "errors" + "fmt" + "os" + "testing" +) + +func TestIs(t *testing.T) { + err1 := errors.New("1") + erra := wrapped{"wrap 2", err1} + errb := wrapped{"wrap 3", erra} + erro := errors.Opaque(err1) + errco := wrapped{"opaque", erro} + + err3 := errors.New("3") + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + target error + match bool + }{ + {nil, nil, true}, + {err1, nil, false}, + {err1, err1, true}, + {erra, err1, true}, + {errb, err1, true}, + {errco, erro, true}, + {errco, err1, false}, + {erro, erro, true}, + {err1, err3, false}, + {erra, err3, false}, + {errb, err3, false}, + {poser, err1, true}, + {poser, err3, true}, + {poser, erra, false}, + {poser, errb, false}, + {poser, erro, false}, + {poser, errco, false}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + if got := errors.Is(tc.err, tc.target); got != tc.match { + t.Errorf("Is(%v, %v) = %v, want %v", tc.err, tc.target, got, tc.match) + } + }) + } +} + +type poser struct { + msg string + f func(error) bool +} + +func (p *poser) Error() string { return p.msg } +func (p *poser) Is(err error) bool { return p.f(err) } +func (p *poser) As(err interface{}) bool { + switch x := err.(type) { + case **poser: + *x = p + case *errorT: + *x = errorT{} + case **os.PathError: + *x = &os.PathError{} + default: + return false + } + return true +} + +func TestAs(t *testing.T) { + var errT errorT + var errP *os.PathError + var timeout interface{ Timeout() bool } + var p *poser + _, errF := os.Open("non-existing") + + testCases := []struct { + err error + target interface{} + match bool + }{{ + wrapped{"pittied the fool", errorT{}}, + &errT, + true, + }, { + errF, + &errP, + true, + }, { + errors.Opaque(errT), + &errT, + false, + }, { + errorT{}, + &errP, + false, + }, { + wrapped{"wrapped", nil}, + &errT, + false, + }, { + &poser{"error", nil}, + &errT, + true, + }, { + &poser{"path", nil}, + &errP, + true, + }, { + &poser{"oh no", nil}, + &p, + true, + }, { + errors.New("err"), + &timeout, + false, + }, { + errF, + &timeout, + true, + }, { + wrapped{"path error", errF}, + &timeout, + true, + }} + for i, tc := range testCases { + name := fmt.Sprintf("%d:As(Errorf(..., %v), %v)", i, tc.err, tc.target) + t.Run(name, func(t *testing.T) { + match := errors.As(tc.err, tc.target) + if match != tc.match { + t.Fatalf("match: got %v; want %v", match, tc.match) + } + if !match { + return + } + if tc.target == nil { + t.Fatalf("non-nil result after match") + } + }) + } +} + +func TestAsValidation(t *testing.T) { + var s string + testCases := []interface{}{ + nil, + (*int)(nil), + "error", + &s, + } + err := errors.New("error") + for _, tc := range testCases { + t.Run(fmt.Sprintf("%T(%v)", tc, tc), func(t *testing.T) { + defer func() { + recover() + }() + if errors.As(err, tc) { + t.Errorf("As(err, %T(%v)) = true, want false", tc, tc) + return + } + t.Errorf("As(err, %T(%v)) did not panic", tc, tc) + }) + } +} + +func TestUnwrap(t *testing.T) { + err1 := errors.New("1") + erra := wrapped{"wrap 2", err1} + erro := errors.Opaque(err1) + + testCases := []struct { + err error + want error + }{ + {nil, nil}, + {wrapped{"wrapped", nil}, nil}, + {err1, nil}, + {erra, err1}, + {wrapped{"wrap 3", erra}, erra}, + + {erro, nil}, + {wrapped{"opaque", erro}, erro}, + } + for _, tc := range testCases { + if got := errors.Unwrap(tc.err); got != tc.want { + t.Errorf("Unwrap(%v) = %v, want %v", tc.err, got, tc.want) + } + } +} + +func TestOpaque(t *testing.T) { + someError := errors.New("some error") + testCases := []struct { + err error + next error + }{ + {errorT{}, nil}, + {wrapped{"b", nil}, nil}, + {wrapped{"c", someError}, someError}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + opaque := errors.Opaque(tc.err) + + f, ok := opaque.(errors.Formatter) + if !ok { + t.Fatal("Opaque error does not implement Formatter") + } + var p printer + next := f.FormatError(&p) + if next != tc.next { + t.Errorf("next was %v; want %v", next, tc.next) + } + if got, want := p.buf.String(), tc.err.Error(); got != want { + t.Errorf("error was %q; want %q", got, want) + } + if got := errors.Unwrap(opaque); got != nil { + t.Errorf("Unwrap returned non-nil error (%v)", got) + } + }) + } +} + +type errorT struct{} + +func (errorT) Error() string { return "errorT" } + +type wrapped struct { + msg string + err error +} + +func (e wrapped) Error() string { return e.msg } + +func (e wrapped) Unwrap() error { return e.err } + +func (e wrapped) FormatError(p errors.Printer) error { + p.Print(e.msg) + return e.err +} + +type printer struct { + errors.Printer + buf bytes.Buffer +} + +func (p *printer) Print(args ...interface{}) { fmt.Fprint(&p.buf, args...) } |