Merge pull request #27 from mdeggies/add-circleci
Add CircleCI & remove travis
diff --git a/README.md b/README.md
index ead5830..074a749 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[][godocs]
[travis]: https://travis-ci.org/hashicorp/go-multierror
-[godocs]: https://godoc.org/github.com/hashicorp/go-multierror
+[godocs]: https://pkg.go.dev/github.com/hashicorp/go-multierror
`go-multierror` is a package for Go that provides a mechanism for
representing a list of `error` values as a single `error`.
@@ -14,16 +14,17 @@
list and access the errors. If the caller doesn't know, the error
formats to a nice human-readable format.
-`go-multierror` implements the
-[errwrap](https://github.com/hashicorp/errwrap) interface so that it can
-be used with that library, as well.
+`go-multierror` is fully compatible with the Go standard library
+[errors](https://golang.org/pkg/errors/) package, including the
+functions `As`, `Is`, and `Unwrap`. This provides a standardized approach
+for introspecting on error values.
## Installation and Docs
Install using `go get github.com/hashicorp/go-multierror`.
Full documentation is available at
-http://godoc.org/github.com/hashicorp/go-multierror
+https://pkg.go.dev/github.com/hashicorp/go-multierror
## Usage
@@ -81,6 +82,39 @@
}
```
+You can also use the standard [`errors.Unwrap`](https://golang.org/pkg/errors/#Unwrap)
+function. This will continue to unwrap into subsequent errors until none exist.
+
+**Extracting an error**
+
+The standard library [`errors.As`](https://golang.org/pkg/errors/#As)
+function can be used directly with a multierror to extract a specific error:
+
+```go
+// Assume err is a multierror value
+err := somefunc()
+
+// We want to know if "err" has a "RichErrorType" in it and extract it.
+var errRich RichErrorType
+if errors.As(err, &errRich) {
+ // It has it, and now errRich is populated.
+}
+```
+
+**Checking for an exact error value**
+
+Some errors are returned as exact errors such as the [`ErrNotExist`](https://golang.org/pkg/os/#pkg-variables)
+error in the `os` package. You can check if this error is present by using
+the standard [`errors.Is`](https://golang.org/pkg/errors/#Is) function.
+
+```go
+// Assume err is a multierror value
+err := somefunc()
+if errors.Is(err, os.ErrNotExist) {
+ // err contains os.ErrNotExist
+}
+```
+
**Returning a multierror only if there are errors**
If you build a `multierror.Error`, you can use the `ErrorOrNil` function
diff --git a/append.go b/append.go
index 775b6e7..3e2589b 100644
--- a/append.go
+++ b/append.go
@@ -6,6 +6,8 @@
// If err is not a multierror.Error, then it will be turned into
// one. If any of the errs are multierr.Error, they will be flattened
// one level into err.
+// Any nil errors within errs will be ignored. If err is nil, a new
+// *Error will be returned.
func Append(err error, errs ...error) *Error {
switch err := err.(type) {
case *Error:
diff --git a/go.mod b/go.mod
index 2534331..0afe8e6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module github.com/hashicorp/go-multierror
+go 1.14
+
require github.com/hashicorp/errwrap v1.0.0
diff --git a/group.go b/group.go
new file mode 100644
index 0000000..9c29efb
--- /dev/null
+++ b/group.go
@@ -0,0 +1,38 @@
+package multierror
+
+import "sync"
+
+// Group is a collection of goroutines which return errors that need to be
+// coalesced.
+type Group struct {
+ mutex sync.Mutex
+ err *Error
+ wg sync.WaitGroup
+}
+
+// Go calls the given function in a new goroutine.
+//
+// If the function returns an error it is added to the group multierror which
+// is returned by Wait.
+func (g *Group) Go(f func() error) {
+ g.wg.Add(1)
+
+ go func() {
+ defer g.wg.Done()
+
+ if err := f(); err != nil {
+ g.mutex.Lock()
+ g.err = Append(g.err, err)
+ g.mutex.Unlock()
+ }
+ }()
+}
+
+// Wait blocks until all function calls from the Go method have returned, then
+// returns the multierror.
+func (g *Group) Wait() *Error {
+ g.wg.Wait()
+ g.mutex.Lock()
+ defer g.mutex.Unlock()
+ return g.err
+}
diff --git a/group_test.go b/group_test.go
new file mode 100644
index 0000000..9d472fd
--- /dev/null
+++ b/group_test.go
@@ -0,0 +1,44 @@
+package multierror
+
+import (
+ "errors"
+ "strings"
+ "testing"
+)
+
+func TestGroup(t *testing.T) {
+ err1 := errors.New("group_test: 1")
+ err2 := errors.New("group_test: 2")
+
+ cases := []struct {
+ errs []error
+ nilResult bool
+ }{
+ {errs: []error{}, nilResult: true},
+ {errs: []error{nil}, nilResult: true},
+ {errs: []error{err1}},
+ {errs: []error{err1, nil}},
+ {errs: []error{err1, nil, err2}},
+ }
+
+ for _, tc := range cases {
+ var g Group
+
+ for _, err := range tc.errs {
+ err := err
+ g.Go(func() error { return err })
+
+ }
+
+ gErr := g.Wait()
+ if gErr != nil {
+ for i := range tc.errs {
+ if tc.errs[i] != nil && !strings.Contains(gErr.Error(), tc.errs[i].Error()) {
+ t.Fatalf("expected error to contain %q, actual: %v", tc.errs[i].Error(), gErr)
+ }
+ }
+ } else if !tc.nilResult {
+ t.Fatalf("Group.Wait() should not have returned nil for errs: %v", tc.errs)
+ }
+ }
+}
diff --git a/multierror.go b/multierror.go
index 89b1422..d05dd92 100644
--- a/multierror.go
+++ b/multierror.go
@@ -1,6 +1,7 @@
package multierror
import (
+ "errors"
"fmt"
)
@@ -49,3 +50,69 @@
func (e *Error) WrappedErrors() []error {
return e.Errors
}
+
+// Unwrap returns an error from Error (or nil if there are no errors).
+// This error returned will further support Unwrap to get the next error,
+// etc. The order will match the order of Errors in the multierror.Error
+// at the time of calling.
+//
+// The resulting error supports errors.As/Is/Unwrap so you can continue
+// to use the stdlib errors package to introspect further.
+//
+// This will perform a shallow copy of the errors slice. Any errors appended
+// to this error after calling Unwrap will not be available until a new
+// Unwrap is called on the multierror.Error.
+func (e *Error) Unwrap() error {
+ // If we have no errors then we do nothing
+ if e == nil || len(e.Errors) == 0 {
+ return nil
+ }
+
+ // If we have exactly one error, we can just return that directly.
+ if len(e.Errors) == 1 {
+ return e.Errors[0]
+ }
+
+ // Shallow copy the slice
+ errs := make([]error, len(e.Errors))
+ copy(errs, e.Errors)
+ return chain(errs)
+}
+
+// chain implements the interfaces necessary for errors.Is/As/Unwrap to
+// work in a deterministic way with multierror. A chain tracks a list of
+// errors while accounting for the current represented error. This lets
+// Is/As be meaningful.
+//
+// Unwrap returns the next error. In the cleanest form, Unwrap would return
+// the wrapped error here but we can't do that if we want to properly
+// get access to all the errors. Instead, users are recommended to use
+// Is/As to get the correct error type out.
+//
+// Precondition: []error is non-empty (len > 0)
+type chain []error
+
+// Error implements the error interface
+func (e chain) Error() string {
+ return e[0].Error()
+}
+
+// Unwrap implements errors.Unwrap by returning the next error in the
+// chain or nil if there are no more errors.
+func (e chain) Unwrap() error {
+ if len(e) == 1 {
+ return nil
+ }
+
+ return e[1:]
+}
+
+// As implements errors.As by attempting to map to the current value.
+func (e chain) As(target interface{}) bool {
+ return errors.As(e[0], target)
+}
+
+// Is implements errors.Is by comparing the current value directly.
+func (e chain) Is(target error) bool {
+ return errors.Is(e[0], target)
+}
diff --git a/multierror_test.go b/multierror_test.go
index 2949c3b..972c52d 100644
--- a/multierror_test.go
+++ b/multierror_test.go
@@ -2,6 +2,7 @@
import (
"errors"
+ "fmt"
"reflect"
"testing"
)
@@ -69,3 +70,134 @@
t.Fatalf("bad: %s", multi.WrappedErrors())
}
}
+
+func TestErrorUnwrap(t *testing.T) {
+ t.Run("with errors", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ errors.New("bar"),
+ errors.New("baz"),
+ }}
+
+ var current error = err
+ for i := 0; i < len(err.Errors); i++ {
+ current = errors.Unwrap(current)
+ if !errors.Is(current, err.Errors[i]) {
+ t.Fatal("should be next value")
+ }
+ }
+
+ if errors.Unwrap(current) != nil {
+ t.Fatal("should be nil at the end")
+ }
+ })
+
+ t.Run("with no errors", func(t *testing.T) {
+ err := &Error{Errors: nil}
+ if errors.Unwrap(err) != nil {
+ t.Fatal("should be nil")
+ }
+ })
+
+ t.Run("with nil multierror", func(t *testing.T) {
+ var err *Error
+ if errors.Unwrap(err) != nil {
+ t.Fatal("should be nil")
+ }
+ })
+}
+
+func TestErrorIs(t *testing.T) {
+ errBar := errors.New("bar")
+
+ t.Run("with errBar", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ errBar,
+ errors.New("baz"),
+ }}
+
+ if !errors.Is(err, errBar) {
+ t.Fatal("should be true")
+ }
+ })
+
+ t.Run("with errBar wrapped by fmt.Errorf", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ fmt.Errorf("errorf: %w", errBar),
+ errors.New("baz"),
+ }}
+
+ if !errors.Is(err, errBar) {
+ t.Fatal("should be true")
+ }
+ })
+
+ t.Run("without errBar", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ errors.New("baz"),
+ }}
+
+ if errors.Is(err, errBar) {
+ t.Fatal("should be false")
+ }
+ })
+}
+
+func TestErrorAs(t *testing.T) {
+ match := &nestedError{}
+
+ t.Run("with the value", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ match,
+ errors.New("baz"),
+ }}
+
+ var target *nestedError
+ if !errors.As(err, &target) {
+ t.Fatal("should be true")
+ }
+ if target == nil {
+ t.Fatal("target should not be nil")
+ }
+ })
+
+ t.Run("with the value wrapped by fmt.Errorf", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ fmt.Errorf("errorf: %w", match),
+ errors.New("baz"),
+ }}
+
+ var target *nestedError
+ if !errors.As(err, &target) {
+ t.Fatal("should be true")
+ }
+ if target == nil {
+ t.Fatal("target should not be nil")
+ }
+ })
+
+ t.Run("without the value", func(t *testing.T) {
+ err := &Error{Errors: []error{
+ errors.New("foo"),
+ errors.New("baz"),
+ }}
+
+ var target *nestedError
+ if errors.As(err, &target) {
+ t.Fatal("should be false")
+ }
+ if target != nil {
+ t.Fatal("target should be nil")
+ }
+ })
+}
+
+// nestedError implements error and is used for tests.
+type nestedError struct{}
+
+func (*nestedError) Error() string { return "" }