Repository: grafana/dskit (go)

Issue:
Allow casting multierror.MultiError to the types of its composing errors
It is currently possible to verify if a specific error has been added to the list of errors represented by an object of type `multierror.MultiError` by using `errors.Is()` function. 

For example, assuming that there is a type `customError` implementing the `error` interface, both variables `isCustomError` and `isContextCanceled` will be `true` at the end of the following code snippet:
```
cErr              := newCustomError()
mErr              := multierror.New(context.Canceled, cErr).Err()
isCustomError     := errors.Is(mErr, cErr)             // true
isContextCanceled := errors.Is(mErr, context.Canceled) // true
```

Using `multierror.MultError` withing `errors.As()`, though, do not provide correct results: `multierror.MultiError` represents a list of errors, each of a certain error type, but `multierror.MultiError` cannot be cast to any of those error types.

The problem can be illustrated by using the example from above: although `mErr` contains error `cErr` of type `customError`, `errors.As()` returns false.
```
cErr := newCustomError()
mErr := multerror.New(context.Canceled, cErr).Err()
var target customError
isCast := errors.As(mErr, &target) // false
```

The goal of this issue is to fix that problem.

Pull request:
multierror: Add support for errors.As()
**What this PR does**:
This PR allows that both `errors.As()` can be correctly used with `multierror.MultiError`. 
Please consult the issue for more details.

This is achieved by implementing function `Unwrap() []error`. Since `Unwrap()` covers both `errors.Is()` and `errors.As()`, the implementation of `Is(error) bool` added in #254 has been removed.

**Which issue(s) this PR fixes**:

Fixes #521 

**Checklist**
- [x] Tests updated
- [x] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d61365..2d74fba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Changelog
 
+* [CHANGE] Multierror: Implement `Unwrap() []error`. This allows to use `multierror.MultiError` with both `errors.Is()` and `errors.As()`. Previously implemented `Is(error) bool` has been removed. #522
 * [CHANGE] Add a new `grpc_server_recv_buffer_pools_enabled` option that enables recv buffer pools in the gRPC server (assuming `grpc_server_stats_tracking_enabled` is disabled). #510
 * [CHANGE] Add a new `grpc_server_stats_tracking_enabled` option that allows us to disable stats tracking and potentially improve server memory reuse. #507
 * [CHANGE] Add support for PROXY protocol to Server. #508
diff --git a/multierror/multierror.go b/multierror/multierror.go
index 68b73e2..d48b76e 100644
--- a/multierror/multierror.go
+++ b/multierror/multierror.go
@@ -5,7 +5,6 @@ package multierror
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 )
 
@@ -62,14 +61,6 @@ func (es nonNilMultiError) Error() string {
 	return buf.String()
 }
 
-// Is attempts to match the provided error against errors in the error list.
-//
-// This function allows errors.Is to traverse the values stored in the MultiError.
-func (es nonNilMultiError) Is(target error) bool {
-	for _, err := range es {
-		if errors.Is(err, target) {
-			return true
-		}
-	}
-	return false
+func (es nonNilMultiError) Unwrap() []error {
+	return es
 }


Test patch:
diff --git a/multierror/multierror_test.go b/multierror/multierror_test.go
index f757257..d2bc33f 100644
--- a/multierror/multierror_test.go
+++ b/multierror/multierror_test.go
@@ -2,10 +2,12 @@ package multierror
 
 import (
 	"context"
+	"fmt"
 	"testing"
 
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestMultiError_Is(t *testing.T) {
@@ -34,6 +36,11 @@ func TestMultiError_Is(t *testing.T) {
 			target:       context.Canceled,
 			is:           true,
 		},
+		"errors with no context cancellation error are not a context canceled error": {
+			sourceErrors: []error{errors.New("first error"), errors.New("second error")},
+			target:       context.Canceled,
+			is:           false,
+		},
 		"no errors are not a context canceled error": {
 			sourceErrors: nil,
 			target:       context.Canceled,
@@ -48,11 +55,59 @@ func TestMultiError_Is(t *testing.T) {
 
 	for testName, testCase := range testCases {
 		t.Run(testName, func(t *testing.T) {
-			mErr := MultiError{}
-			for _, err := range testCase.sourceErrors {
-				mErr.Add(err)
-			}
+			mErr := New(testCase.sourceErrors...)
 			assert.Equal(t, testCase.is, errors.Is(mErr.Err(), testCase.target))
 		})
 	}
 }
+
+func TestMultiError_As(t *testing.T) {
+	tE1 := testError{"error cause 1"}
+	tE2 := testError{"error cause 2"}
+	var target testError
+	testCases := map[string]struct {
+		sourceErrors []error
+		target       error
+		as           bool
+	}{
+		"MultiError containing only a testError can be cast to that testError": {
+			sourceErrors: []error{tE1},
+			target:       tE1,
+			as:           true,
+		},
+		"MultiError containing multiple testErrors can be cast to the first testError added": {
+			sourceErrors: []error{tE1, tE2},
+			target:       tE1,
+			as:           true,
+		},
+		"MultiError containing multiple errors can be cast to the first testError added": {
+			sourceErrors: []error{context.Canceled, tE1, context.DeadlineExceeded, tE2},
+			target:       tE1,
+			as:           true,
+		},
+		"MultiError non containing a testError cannot be cast to a testError": {
+			sourceErrors: []error{context.Canceled, context.DeadlineExceeded},
+			as:           false,
+		},
+	}
+
+	for testName, testCase := range testCases {
+		t.Run(testName, func(t *testing.T) {
+			mErr := New(testCase.sourceErrors...).Err()
+			if testCase.as {
+				require.True(t, errors.As(mErr, &target))
+				require.Equal(t, testCase.target, target)
+			} else {
+				require.False(t, errors.As(mErr, &target))
+			}
+		})
+	}
+}
+
+type testError struct {
+	cause string
+}
+
+func (e testError) Error() string {
+	return fmt.Sprintf("testError[cause: %s]", e.cause)
+}