From 616f4860ca0a81fc1aa4f225e13d8a82944f293e Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Wed, 11 Oct 2023 11:46:30 -0700 Subject: [PATCH] chore: Drop support for Go < 1.20 (#80) Drops support for versions of Go older than 1.20. With 1.20 being the minimum supported Go version, we can remove the pre_go120 code, and merge the post_go120 code. With Go 1.20's multi-error interface, our errorGroup interface is not necessary, but we need it for backwards compatibility. Also updates the documentation to suggest Go 1.20's interface instead of ours. The `Errors() []error` function is now an implementation detail. --- .github/workflows/go.yml | 4 +- error.go | 70 +++++++++++++++++++++++++---------- error_post_go120.go | 48 ------------------------ error_post_go120_test.go | 65 --------------------------------- error_pre_go120.go | 79 ---------------------------------------- error_test.go | 36 +++++++++++++++++- go.mod | 2 +- tools/go.mod | 2 +- 8 files changed, 89 insertions(+), 217 deletions(-) delete mode 100644 error_post_go120.go delete mode 100644 error_post_go120_test.go delete mode 100644 error_pre_go120.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2798326..7d4c7cb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,9 +16,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ["1.19.x", "1.20.x"] + go: ["1.20.x", "1.21.x"] include: - - go: 1.20.x + - go: 1.21.x latest: true steps: diff --git a/error.go b/error.go index 3a828b2..a70ae00 100644 --- a/error.go +++ b/error.go @@ -115,24 +115,22 @@ // # Advanced Usage // // Errors returned by Combine and Append MAY implement the following -// interface. +// method. // -// type errorGroup interface { -// // Returns a slice containing the underlying list of errors. -// // -// // This slice MUST NOT be modified by the caller. -// Errors() []error -// } +// // Returns a slice containing the underlying list of errors. +// // +// // This slice MUST NOT be modified by the caller. +// Unwrap() []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 +// 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) +// group, ok := err.(interface{ Unwrap() []error }) // if ok { // errors = group.Errors() // } else { @@ -180,10 +178,20 @@ var _bufferPool = sync.Pool{ }, } +// errorGroup is the old interface defined by multierr for combined errors. +// +// It is deprecated in favor of the Go 1.20 multi-error interface. +// However, we still need to implement and support it +// for backward compatibility. type errorGroup interface { Errors() []error } +// multipleErrors matches the Go 1.20 multi-error interface. +type multipleErrors interface { + Unwrap() []error +} + // Errors returns a slice containing zero or more errors that the supplied // error is composed of. If the error is nil, a nil slice is returned. // @@ -210,6 +218,13 @@ type multiError struct { errors []error } +// Unwrap returns a list of errors wrapped by this multierr. +// +// This satisfies the Go 1.20 multi-error interface. +func (merr *multiError) Unwrap() []error { + return merr.Errors() +} + // Errors returns the list of underlying errors. // // This slice MUST NOT be modified. @@ -235,17 +250,6 @@ func (merr *multiError) Error() string { return result } -// Every compares every error in the given err against the given target error -// using [errors.Is], and returns true only if every comparison returned true. -func Every(err error, target error) bool { - for _, e := range extractErrors(err) { - if !errors.Is(e, target) { - return false - } - } - return true -} - func (merr *multiError) Format(f fmt.State, c rune) { if c == 'v' && f.Flag('+') { merr.writeMultiline(f) @@ -508,6 +512,32 @@ func AppendInto(into *error, err error) (errored bool) { return true } +// Every compares every error in the given err against the given target error +// using [errors.Is], and returns true only if every comparison returned true. +func Every(err error, target error) bool { + for _, e := range extractErrors(err) { + if !errors.Is(e, target) { + return false + } + } + return true +} + +func extractErrors(err error) []error { + if err == nil { + return nil + } + + // check if the given err is an Unwrapable error that + // implements multipleErrors interface. + eg, ok := err.(multipleErrors) + if !ok { + return []error{err} + } + + return append(([]error)(nil), eg.Unwrap()...) +} + // Invoker is an operation that may fail with an error. Use it with // AppendInvoke to append the result of calling the function into an error. // This allows you to conveniently defer capture of failing operations. diff --git a/error_post_go120.go b/error_post_go120.go deleted file mode 100644 index a173f9c..0000000 --- a/error_post_go120.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2017-2023 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//go:build go1.20 -// +build go1.20 - -package multierr - -// Unwrap returns a list of errors wrapped by this multierr. -func (merr *multiError) Unwrap() []error { - return merr.Errors() -} - -type multipleErrors interface { - Unwrap() []error -} - -func extractErrors(err error) []error { - if err == nil { - return nil - } - - // check if the given err is an Unwrapable error that - // implements multipleErrors interface. - eg, ok := err.(multipleErrors) - if !ok { - return []error{err} - } - - return append(([]error)(nil), eg.Unwrap()...) -} diff --git a/error_post_go120_test.go b/error_post_go120_test.go deleted file mode 100644 index 39873c2..0000000 --- a/error_post_go120_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2023 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//go:build go1.20 -// +build go1.20 - -package multierr - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrorsOnErrorsJoin(t *testing.T) { - err1 := errors.New("err1") - err2 := errors.New("err2") - err := errors.Join(err1, err2) - - errs := Errors(err) - assert.Equal(t, 2, len(errs)) - assert.Equal(t, err1, errs[0]) - assert.Equal(t, err2, errs[1]) -} - -func TestEveryWithErrorsJoin(t *testing.T) { - myError1 := errors.New("woeful misfortune") - myError2 := errors.New("worrisome travesty") - - t.Run("all match", func(t *testing.T) { - err := errors.Join(myError1, myError1, myError1) - - assert.True(t, errors.Is(err, myError1)) - assert.True(t, Every(err, myError1)) - assert.False(t, errors.Is(err, myError2)) - assert.False(t, Every(err, myError2)) - }) - - t.Run("one matches", func(t *testing.T) { - err := errors.Join(myError1, myError2) - - assert.True(t, errors.Is(err, myError1)) - assert.False(t, Every(err, myError1)) - assert.True(t, errors.Is(err, myError2)) - assert.False(t, Every(err, myError2)) - }) -} diff --git a/error_pre_go120.go b/error_pre_go120.go deleted file mode 100644 index 93872a3..0000000 --- a/error_pre_go120.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2017-2023 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//go:build !go1.20 -// +build !go1.20 - -package multierr - -import "errors" - -// Versions of Go before 1.20 did not support the Unwrap() []error method. -// This provides a similar behavior by implementing the Is(..) and As(..) -// methods. -// See the errors.Join proposal for details: -// https://github.com/golang/go/issues/53435 - -// As attempts to find the first error in the error list that matches the type -// of the value that target points to. -// -// This function allows errors.As to traverse the values stored on the -// multierr error. -func (merr *multiError) As(target interface{}) bool { - for _, err := range merr.Errors() { - if errors.As(err, target) { - return true - } - } - return false -} - -// Is attempts to match the provided error against errors in the error list. -// -// This function allows errors.Is to traverse the values stored on the -// multierr error. -func (merr *multiError) Is(target error) bool { - for _, err := range merr.Errors() { - if errors.Is(err, target) { - return true - } - } - return false -} - -func extractErrors(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} - } - - return append(([]error)(nil), eg.Errors()...) -} diff --git a/error_test.go b/error_test.go index 2e02d04..135a738 100644 --- a/error_test.go +++ b/error_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017-2021 Uber Technologies, Inc. +// Copyright (c) 2017-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -777,3 +777,37 @@ func newCloserMock(tb testing.TB, err error) io.Closer { return err }) } + +func TestErrorsOnErrorsJoin(t *testing.T) { + err1 := errors.New("err1") + err2 := errors.New("err2") + err := errors.Join(err1, err2) + + errs := Errors(err) + assert.Equal(t, 2, len(errs)) + assert.Equal(t, err1, errs[0]) + assert.Equal(t, err2, errs[1]) +} + +func TestEveryWithErrorsJoin(t *testing.T) { + myError1 := errors.New("woeful misfortune") + myError2 := errors.New("worrisome travesty") + + t.Run("all match", func(t *testing.T) { + err := errors.Join(myError1, myError1, myError1) + + assert.True(t, errors.Is(err, myError1)) + assert.True(t, Every(err, myError1)) + assert.False(t, errors.Is(err, myError2)) + assert.False(t, Every(err, myError2)) + }) + + t.Run("one matches", func(t *testing.T) { + err := errors.Join(myError1, myError2) + + assert.True(t, errors.Is(err, myError1)) + assert.False(t, Every(err, myError1)) + assert.True(t, errors.Is(err, myError2)) + assert.False(t, Every(err, myError2)) + }) +} diff --git a/go.mod b/go.mod index 1b4e0f5..349a68f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.uber.org/multierr -go 1.19 +go 1.20 require github.com/stretchr/testify v1.7.0 diff --git a/tools/go.mod b/tools/go.mod index d6190ae..4dc8e34 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,6 +1,6 @@ module go.uber.org/multierr/tools -go 1.18 +go 1.20 require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616