From c47578b528770357ef0d7653ed556c8a5ae3db94 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 12 Dec 2022 16:48:11 -0800 Subject: [PATCH] util/multierr: add Range (#6643) Errors in Go are no longer viewed as a linear chain, but a tree. See golang/go#53435. Add a Range function that iterates through an error in a pre-order, depth-first order. This matches the iteration order of errors.As in Go 1.20. This adds the logic (but currently commented out) for having Error implement the multi-error version of Unwrap in Go 1.20. It is commented out currently since it causes "go vet" to complain about having the "wrong" signature. Signed-off-by: Joe Tsai --- util/multierr/multierr.go | 49 +++++++++++++++++++++++++++++++++- util/multierr/multierr_test.go | 28 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/util/multierr/multierr.go b/util/multierr/multierr.go index 7db592117..83a5cb17f 100644 --- a/util/multierr/multierr.go +++ b/util/multierr/multierr.go @@ -9,6 +9,8 @@ import ( "errors" "strings" + + "golang.org/x/exp/slices" ) // An Error represents multiple errors. @@ -29,9 +31,19 @@ func (e Error) Error() string { // Errors returns a slice containing all errors in e. func (e Error) Errors() []error { - return append(e.errs[:0:0], e.errs...) + return slices.Clone(e.errs) } +// TODO(https://go.dev/cl/53435): Implement Unwrap when Go 1.20 is released. +/* +// Unwrap returns the underlying errors as is. +func (e Error) Unwrap() []error { + // Do not clone since Unwrap requires callers to not mutate the slice. + // See the documentation in the Go "errors" package. + return e.errs +} +*/ + // New returns an error composed from errs. // Some errors in errs get special treatment: // - nil errors are discarded @@ -87,3 +99,38 @@ func (e Error) As(target any) bool { } return false } + +// Range performs a pre-order, depth-first iteration of the error tree +// by successively unwrapping all error values. +// For each iteration it calls fn with the current error value and +// stops iteration if it ever reports false. +func Range(err error, fn func(error) bool) bool { + if err == nil { + return true + } + if !fn(err) { + return false + } + switch err := err.(type) { + case interface{ Unwrap() error }: + if err := err.Unwrap(); err != nil { + if !Range(err, fn) { + return false + } + } + case interface{ Unwrap() []error }: + for _, err := range err.Unwrap() { + if !Range(err, fn) { + return false + } + } + // TODO(https://go.dev/cl/53435): Delete this when Error implements Unwrap. + case Error: + for _, err := range err.errs { + if !Range(err, fn) { + return false + } + } + } + return true +} diff --git a/util/multierr/multierr_test.go b/util/multierr/multierr_test.go index 8f5e20541..f8cb729ba 100644 --- a/util/multierr/multierr_test.go +++ b/util/multierr/multierr_test.go @@ -6,9 +6,11 @@ import ( "errors" + "fmt" "testing" qt "github.com/frankban/quicktest" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/util/multierr" ) @@ -78,3 +80,29 @@ func TestAll(t *testing.T) { C.Assert(ee.Is(x), qt.IsFalse) } } + +func TestRange(t *testing.T) { + C := qt.New(t) + + errA := errors.New("A") + errB := errors.New("B") + errC := errors.New("C") + errD := errors.New("D") + errCD := multierr.New(errC, errD) + errCD1 := fmt.Errorf("1:%w", errCD) + errE := errors.New("E") + errE1 := fmt.Errorf("1:%w", errE) + errE2 := fmt.Errorf("2:%w", errE1) + errF := errors.New("F") + root := multierr.New(errA, errB, errCD1, errE2, errF) + + var got []error + want := []error{root, errA, errB, errCD1, errCD, errC, errD, errE2, errE1, errE, errF} + multierr.Range(root, func(err error) bool { + got = append(got, err) + return true + }) + C.Assert(got, qt.CmpEquals(cmp.Comparer(func(x, y error) bool { + return x.Error() == y.Error() + })), want) +}