mirror of https://github.com/uber-go/multierr.git
Support extracting underlying error list (#19)
This change adds an `Errors(err) []error` function which returns the underlying list of errors. Additionally, it amends the contract for returned errors that they MAY implement a specific interface. This is needed for Zap integration as per discussion in #6. Resolves #10.
This commit is contained in:
parent
87edb7a595
commit
8aa0ee0efd
90
error.go
90
error.go
|
|
@ -20,6 +20,8 @@
|
|||
|
||||
// Package multierr allows combining one or more errors together.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// Errors can be combined with the use of the Combine function.
|
||||
//
|
||||
// multierr.Combine(
|
||||
|
|
@ -46,6 +48,41 @@
|
|||
// }()
|
||||
// // ...
|
||||
// }
|
||||
//
|
||||
// The underlying list of errors for a returned error object may be retrieved
|
||||
// with the Errors function.
|
||||
//
|
||||
// errors := multierr.Errors(err)
|
||||
// if len(errors) > 0 {
|
||||
// fmt.Println("The following errors occurred:")
|
||||
// }
|
||||
//
|
||||
// Advanced Usage
|
||||
//
|
||||
// Errors returned by Combine and Append MAY implement the following
|
||||
// interface.
|
||||
//
|
||||
// type errorGroup interface {
|
||||
// // Returns a slice containing the underlying list of errors.
|
||||
// //
|
||||
// // This slice MUST NOT be modified by the caller.
|
||||
// Errors() []error
|
||||
// }
|
||||
//
|
||||
// Note that if you need access to list of errors behind a multierr error, you
|
||||
// should prefer using the Errors function. That said, if you need cheap
|
||||
// read-only access to the underlying errors slice, you can attempt to cast
|
||||
// the error to this interface. You MUST handle the failure case gracefully
|
||||
// because errors returned by Combine and Append are not guaranteed to
|
||||
// implement this interface.
|
||||
//
|
||||
// var errors []error
|
||||
// group, ok := err.(errorGroup)
|
||||
// if ok {
|
||||
// errors = group.Errors()
|
||||
// } else {
|
||||
// errors = []error{err}
|
||||
// }
|
||||
package multierr // import "go.uber.org/multierr"
|
||||
|
||||
import (
|
||||
|
|
@ -90,6 +127,43 @@ var _bufferPool = sync.Pool{
|
|||
},
|
||||
}
|
||||
|
||||
type errorGroup interface {
|
||||
Errors() []error
|
||||
}
|
||||
|
||||
// Errors returns a slice containing zero or more errors that the supplied
|
||||
// error is composed of. If the error is nil, the returned slice is empty.
|
||||
//
|
||||
// err := multierr.Append(r.Close(), w.Close())
|
||||
// errors := multierr.Errors(err)
|
||||
//
|
||||
// If the error is not composed of other errors, the returned slice contains
|
||||
// just the error that was passed in.
|
||||
//
|
||||
// Callers of this function are free to modify the returned slice.
|
||||
func Errors(err error) []error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note that we're casting to multiError, not errorGroup. Our contract is
|
||||
// that returned errors MAY implement errorGroup. Errors, however, only
|
||||
// has special behavior for multierr-specific error objects.
|
||||
//
|
||||
// This behavior can be expanded in the future but I think it's prudent to
|
||||
// start with as little as possible in terms of contract and possibility
|
||||
// of misuse.
|
||||
eg, ok := err.(*multiError)
|
||||
if !ok {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
errors := eg.Errors()
|
||||
result := make([]error, len(errors))
|
||||
copy(result, errors)
|
||||
return result
|
||||
}
|
||||
|
||||
// multiError is an error that holds one or more errors.
|
||||
//
|
||||
// An instance of this is guaranteed to be non-empty and flattened. That is,
|
||||
|
|
@ -102,6 +176,18 @@ type multiError struct {
|
|||
errors []error
|
||||
}
|
||||
|
||||
var _ errorGroup = (*multiError)(nil)
|
||||
|
||||
// Errors returns the list of underlying errors.
|
||||
//
|
||||
// This slice MUST NOT be modified.
|
||||
func (merr *multiError) Errors() []error {
|
||||
if merr == nil {
|
||||
return nil
|
||||
}
|
||||
return merr.errors
|
||||
}
|
||||
|
||||
func (merr *multiError) Error() string {
|
||||
if merr == nil {
|
||||
return ""
|
||||
|
|
@ -280,8 +366,8 @@ func Combine(errors ...error) error {
|
|||
//
|
||||
// err = multierr.Append(reader.Close(), writer.Close())
|
||||
//
|
||||
// This may be used to record failure of deferred operations without losing
|
||||
// information about the original error.
|
||||
// The following pattern may also be used to record failure of deferred
|
||||
// operations without losing information about the original error.
|
||||
//
|
||||
// func doSomething(..) (err error) {
|
||||
// f := acquireResource()
|
||||
|
|
|
|||
119
error_test.go
119
error_test.go
|
|
@ -345,6 +345,109 @@ func TestAppend(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type notMultiErr struct{}
|
||||
|
||||
var _ errorGroup = notMultiErr{}
|
||||
|
||||
func (notMultiErr) Error() string {
|
||||
return "great sadness"
|
||||
}
|
||||
|
||||
func (notMultiErr) Errors() []error {
|
||||
return []error{errors.New("great sadness")}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
give error
|
||||
want []error
|
||||
|
||||
// Don't attempt to cast to errorGroup or *multiError
|
||||
dontCast bool
|
||||
}{
|
||||
{dontCast: true}, // nil
|
||||
{
|
||||
give: errors.New("hi"),
|
||||
want: []error{errors.New("hi")},
|
||||
dontCast: true,
|
||||
},
|
||||
{
|
||||
// We don't yet support non-multierr errors.
|
||||
give: notMultiErr{},
|
||||
want: []error{notMultiErr{}},
|
||||
dontCast: true,
|
||||
},
|
||||
{
|
||||
give: Combine(
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
),
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
give: Append(
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
),
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
give: Append(
|
||||
errors.New("foo"),
|
||||
Combine(
|
||||
errors.New("bar"),
|
||||
),
|
||||
),
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
give: Combine(
|
||||
errors.New("foo"),
|
||||
Append(
|
||||
errors.New("bar"),
|
||||
errors.New("baz"),
|
||||
),
|
||||
errors.New("qux"),
|
||||
),
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
errors.New("baz"),
|
||||
errors.New("qux"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
t.Run("Errors()", func(t *testing.T) {
|
||||
require.Equal(t, tt.want, Errors(tt.give))
|
||||
})
|
||||
|
||||
if tt.dontCast {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("multiError", func(t *testing.T) {
|
||||
require.Equal(t, tt.want, tt.give.(*multiError).Errors())
|
||||
})
|
||||
|
||||
t.Run("errorGroup", func(t *testing.T) {
|
||||
require.Equal(t, tt.want, tt.give.(errorGroup).Errors())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createMultiErrWithCapacity() error {
|
||||
// Create a multiError that has capacity for more errors so Append will
|
||||
// modify the underlying array that may be shared.
|
||||
|
|
@ -383,10 +486,26 @@ func TestAppendRace(t *testing.T) {
|
|||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestErrorsSliceIsImmutable(t *testing.T) {
|
||||
err1 := errors.New("err1")
|
||||
err2 := errors.New("err2")
|
||||
|
||||
err := Append(err1, err2)
|
||||
gotErrors := Errors(err)
|
||||
require.Equal(t, []error{err1, err2}, gotErrors, "errors must match")
|
||||
|
||||
gotErrors[0] = nil
|
||||
gotErrors[1] = errors.New("err3")
|
||||
|
||||
require.Equal(t, []error{err1, err2}, Errors(err),
|
||||
"errors must match after modification")
|
||||
}
|
||||
|
||||
func TestNilMultierror(t *testing.T) {
|
||||
// For safety, all operations on multiError should be safe even if it is
|
||||
// nil.
|
||||
var err *multiError
|
||||
|
||||
require.Empty(t, err.Error())
|
||||
require.Empty(t, err.Errors())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue