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 <joetsai@digital-static.net>pull/6716/head
parent
041a0e3c27
commit
c47578b528
|
@ -9,6 +9,8 @@ package multierr
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An Error represents multiple errors.
|
// An Error represents multiple errors.
|
||||||
|
@ -29,9 +31,19 @@ func (e Error) Error() string {
|
||||||
|
|
||||||
// Errors returns a slice containing all errors in e.
|
// Errors returns a slice containing all errors in e.
|
||||||
func (e Error) Errors() []error {
|
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.
|
// New returns an error composed from errs.
|
||||||
// Some errors in errs get special treatment:
|
// Some errors in errs get special treatment:
|
||||||
// - nil errors are discarded
|
// - nil errors are discarded
|
||||||
|
@ -87,3 +99,38 @@ func (e Error) As(target any) bool {
|
||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,9 +6,11 @@ package multierr_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"tailscale.com/util/multierr"
|
"tailscale.com/util/multierr"
|
||||||
)
|
)
|
||||||
|
@ -78,3 +80,29 @@ func TestAll(t *testing.T) {
|
||||||
C.Assert(ee.Is(x), qt.IsFalse)
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue