Skip to content

Commit

Permalink
util/multierr: add Error.Unwrap and Range
Browse files Browse the repository at this point in the history
Have Error implement the new interface for Unwrap coming in Go 1.20,
where Unwrap can either return an error or an []error.
See golang/go#53435.

Errors in Go are no longer viewed as a linear chain, but a tree.

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

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
  • Loading branch information
dsnet committed Dec 6, 2022
1 parent 5ff946a commit 5d2ba2b
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 1 deletion.
39 changes: 38 additions & 1 deletion util/multierr/multierr.go
Expand Up @@ -9,6 +9,8 @@ package multierr
import (
"errors"
"strings"

"golang.org/x/exp/slices"
)

// An Error represents multiple errors.
Expand All @@ -29,7 +31,14 @@ 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)
}

// 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.
Expand Down Expand Up @@ -87,3 +96,31 @@ 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
}
}
}
return true
}
28 changes: 28 additions & 0 deletions util/multierr/multierr_test.go
Expand Up @@ -6,9 +6,11 @@ package multierr_test

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"
)
Expand Down Expand Up @@ -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)
}

0 comments on commit 5d2ba2b

Please sign in to comment.