Skip to content

Commit a1dac6d

Browse files
dsnetcoadler
authored andcommitted
util/multierr: add Range (tailscale#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 <[email protected]>
1 parent 03f8524 commit a1dac6d

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

util/multierr/multierr.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package multierr
99
import (
1010
"errors"
1111
"strings"
12+
13+
"golang.org/x/exp/slices"
1214
)
1315

1416
// An Error represents multiple errors.
@@ -29,8 +31,18 @@ func (e Error) Error() string {
2931

3032
// Errors returns a slice containing all errors in e.
3133
func (e Error) Errors() []error {
32-
return append(e.errs[:0:0], e.errs...)
34+
return slices.Clone(e.errs)
35+
}
36+
37+
// TODO(https://go.dev/cl/53435): Implement Unwrap when Go 1.20 is released.
38+
/*
39+
// Unwrap returns the underlying errors as is.
40+
func (e Error) Unwrap() []error {
41+
// Do not clone since Unwrap requires callers to not mutate the slice.
42+
// See the documentation in the Go "errors" package.
43+
return e.errs
3344
}
45+
*/
3446

3547
// New returns an error composed from errs.
3648
// Some errors in errs get special treatment:
@@ -87,3 +99,38 @@ func (e Error) As(target any) bool {
8799
}
88100
return false
89101
}
102+
103+
// Range performs a pre-order, depth-first iteration of the error tree
104+
// by successively unwrapping all error values.
105+
// For each iteration it calls fn with the current error value and
106+
// stops iteration if it ever reports false.
107+
func Range(err error, fn func(error) bool) bool {
108+
if err == nil {
109+
return true
110+
}
111+
if !fn(err) {
112+
return false
113+
}
114+
switch err := err.(type) {
115+
case interface{ Unwrap() error }:
116+
if err := err.Unwrap(); err != nil {
117+
if !Range(err, fn) {
118+
return false
119+
}
120+
}
121+
case interface{ Unwrap() []error }:
122+
for _, err := range err.Unwrap() {
123+
if !Range(err, fn) {
124+
return false
125+
}
126+
}
127+
// TODO(https://go.dev/cl/53435): Delete this when Error implements Unwrap.
128+
case Error:
129+
for _, err := range err.errs {
130+
if !Range(err, fn) {
131+
return false
132+
}
133+
}
134+
}
135+
return true
136+
}

util/multierr/multierr_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package multierr_test
66

77
import (
88
"errors"
9+
"fmt"
910
"testing"
1011

1112
qt "github.com/frankban/quicktest"
13+
"github.com/google/go-cmp/cmp"
1214
"github.com/google/go-cmp/cmp/cmpopts"
1315
"tailscale.com/util/multierr"
1416
)
@@ -78,3 +80,29 @@ func TestAll(t *testing.T) {
7880
C.Assert(ee.Is(x), qt.IsFalse)
7981
}
8082
}
83+
84+
func TestRange(t *testing.T) {
85+
C := qt.New(t)
86+
87+
errA := errors.New("A")
88+
errB := errors.New("B")
89+
errC := errors.New("C")
90+
errD := errors.New("D")
91+
errCD := multierr.New(errC, errD)
92+
errCD1 := fmt.Errorf("1:%w", errCD)
93+
errE := errors.New("E")
94+
errE1 := fmt.Errorf("1:%w", errE)
95+
errE2 := fmt.Errorf("2:%w", errE1)
96+
errF := errors.New("F")
97+
root := multierr.New(errA, errB, errCD1, errE2, errF)
98+
99+
var got []error
100+
want := []error{root, errA, errB, errCD1, errCD, errC, errD, errE2, errE1, errE, errF}
101+
multierr.Range(root, func(err error) bool {
102+
got = append(got, err)
103+
return true
104+
})
105+
C.Assert(got, qt.CmpEquals(cmp.Comparer(func(x, y error) bool {
106+
return x.Error() == y.Error()
107+
})), want)
108+
}

0 commit comments

Comments
 (0)