New Upstream Release - golang-go.uber-multierr

Ready changes

Summary

Merged new upstream version: 1.8.0 (was: 1.6.0).

Resulting package

Built on 2022-04-26T22:06 (took 4m25s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-go.uber-multierr-dev

Lintian Result

Diff

diff --git a/.github/workflows/fossa.yaml b/.github/workflows/fossa.yaml
new file mode 100644
index 0000000..86e6db7
--- /dev/null
+++ b/.github/workflows/fossa.yaml
@@ -0,0 +1,17 @@
+name: FOSSA Analysis
+on: push
+
+jobs:
+
+  build:
+    runs-on: ubuntu-latest
+    if: github.repository_owner == 'uber-go'
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: FOSSA analysis
+        uses: fossas/fossa-action@v1
+        with:
+          api-key: ${{ secrets.FOSSA_API_KEY }}
+
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..7fb612b
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,49 @@
+name: Go
+
+on:
+  push:
+    branches: ['*']
+    tags: ['v*']
+  pull_request:
+    branches: ['*']
+
+jobs:
+
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go: ["1.16.x", "1.17.x"]
+        include:
+        - go: 1.17.x
+          latest: true
+
+    steps:
+    - name: Setup Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ matrix.go }}
+
+    - name: Checkout code
+      uses: actions/checkout@v2
+
+    - name: Load cached dependencies
+      uses: actions/cache@v1
+      with:
+        path: ~/go/pkg/mod
+        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        restore-keys: |
+          ${{ runner.os }}-go-
+
+    - name: Download Dependencies
+      run: go mod download
+
+    - name: Lint
+      if: matrix.latest
+      run: make lint
+
+    - name: Test
+      run: make cover
+
+    - name: Upload coverage to codecov.io
+      uses: codecov/codecov-action@v1
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8636ab4..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-sudo: false
-language: go
-go_import_path: go.uber.org/multierr
-
-env:
-  global:
-    - GO111MODULE=on
-
-go:
-  - oldstable
-  - stable
-
-before_install:
-- go version
-
-script:
-- |
-  set -e
-  make lint
-  make cover
-
-after_success:
-- bash <(curl -s https://codecov.io/bash)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f1db9e..3ba0527 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
 Releases
 ========
 
+v1.8.0 (2022-02-28)
+===================
+
+-   `Combine`: perform zero allocations when there are no errors.
+
+
+v1.7.0 (2021-05-06)
+===================
+
+-   Add `AppendInvoke` to append into errors from `defer` blocks.
+
+
 v1.6.0 (2020-09-14)
 ===================
 
diff --git a/LICENSE.txt b/LICENSE.txt
index 858e024..413e30f 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2017 Uber Technologies, Inc.
+Copyright (c) 2017-2021 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
diff --git a/Makefile b/Makefile
index 3160044..dcb6fe7 100644
--- a/Makefile
+++ b/Makefile
@@ -34,9 +34,5 @@ lint: gofmt golint staticcheck
 
 .PHONY: cover
 cover:
-	go test -coverprofile=cover.out -coverpkg=./... -v ./...
+	go test -race -coverprofile=cover.out -coverpkg=./... -v ./...
 	go tool cover -html=cover.out -o cover.html
-
-update-license:
-	@cd tools && go install go.uber.org/tools/update-license
-	@$(GOBIN)/update-license $(GO_FILES)
diff --git a/README.md b/README.md
index 751bd65..70aacec 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,9 @@ Stable: No breaking changes will be made before 2.0.
 Released under the [MIT License].
 
 [MIT License]: LICENSE.txt
-[doc-img]: https://godoc.org/go.uber.org/multierr?status.svg
-[doc]: https://godoc.org/go.uber.org/multierr
-[ci-img]: https://travis-ci.com/uber-go/multierr.svg?branch=master
+[doc-img]: https://pkg.go.dev/badge/go.uber.org/multierr
+[doc]: https://pkg.go.dev/go.uber.org/multierr
+[ci-img]: https://github.com/uber-go/multierr/actions/workflows/go.yml/badge.svg
 [cov-img]: https://codecov.io/gh/uber-go/multierr/branch/master/graph/badge.svg
-[ci]: https://travis-ci.com/uber-go/multierr
+[ci]: https://github.com/uber-go/multierr/actions/workflows/go.yml
 [cov]: https://codecov.io/gh/uber-go/multierr
diff --git a/appendinvoke_example_test.go b/appendinvoke_example_test.go
new file mode 100644
index 0000000..c2b48f3
--- /dev/null
+++ b/appendinvoke_example_test.go
@@ -0,0 +1,74 @@
+// Copyright (c) 2021 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.
+
+package multierr_test
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+
+	"go.uber.org/multierr"
+)
+
+func ExampleAppendInvoke() {
+	if err := run(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func run() (err error) {
+	dir, err := ioutil.TempDir("", "multierr")
+	// We create a temporary directory and defer its deletion when this
+	// function returns.
+	//
+	// If we failed to delete the temporary directory, we append its
+	// failure into the returned value with multierr.AppendInvoke.
+	//
+	// This uses a custom invoker that we implement below.
+	defer multierr.AppendInvoke(&err, RemoveAll(dir))
+
+	path := filepath.Join(dir, "example.txt")
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	// Similarly, we defer closing the open file when the function returns,
+	// and appends its failure, if any, into the returned error.
+	//
+	// This uses the multierr.Close invoker included in multierr.
+	defer multierr.AppendInvoke(&err, multierr.Close(f))
+
+	if _, err := fmt.Fprintln(f, "hello"); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RemoveAll is a multierr.Invoker that deletes the provided directory and all
+// of its contents.
+type RemoveAll string
+
+func (r RemoveAll) Invoke() error {
+	return os.RemoveAll(string(r))
+}
diff --git a/benchmarks_test.go b/benchmarks_test.go
index 797f5a0..562d3bc 100644
--- a/benchmarks_test.go
+++ b/benchmarks_test.go
@@ -60,3 +60,68 @@ func BenchmarkAppend(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkCombine(b *testing.B) {
+	b.Run("inline 1", func(b *testing.B) {
+		var x error
+		for i := 0; i < b.N; i++ {
+			Combine(x)
+		}
+	})
+
+	b.Run("inline 2", func(b *testing.B) {
+		var x, y error
+		for i := 0; i < b.N; i++ {
+			Combine(x, y)
+		}
+	})
+
+	b.Run("inline 3 no error", func(b *testing.B) {
+		var x, y, z error
+		for i := 0; i < b.N; i++ {
+			Combine(x, y, z)
+		}
+	})
+
+	b.Run("inline 3 one error", func(b *testing.B) {
+		var x, y, z error
+		z = fmt.Errorf("failed")
+		for i := 0; i < b.N; i++ {
+			Combine(x, y, z)
+		}
+	})
+
+	b.Run("inline 3 multiple errors", func(b *testing.B) {
+		var x, y, z error
+		z = fmt.Errorf("failed3")
+		y = fmt.Errorf("failed2")
+		x = fmt.Errorf("failed")
+		for i := 0; i < b.N; i++ {
+			Combine(x, y, z)
+		}
+	})
+
+	b.Run("slice 100 no errors", func(b *testing.B) {
+		errs := make([]error, 100)
+		for i := 0; i < b.N; i++ {
+			Combine(errs...)
+		}
+	})
+
+	b.Run("slice 100 one error", func(b *testing.B) {
+		errs := make([]error, 100)
+		errs[len(errs)-1] = fmt.Errorf("failed")
+		for i := 0; i < b.N; i++ {
+			Combine(errs...)
+		}
+	})
+
+	b.Run("slice 100 multi error", func(b *testing.B) {
+		errs := make([]error, 100)
+		errs[0] = fmt.Errorf("failed1")
+		errs[len(errs)-1] = fmt.Errorf("failed2")
+		for i := 0; i < b.N; i++ {
+			Combine(errs...)
+		}
+	})
+}
diff --git a/debian/changelog b/debian/changelog
index 760c2ee..52ad36b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,12 @@
-golang-go.uber-multierr (1.6.0-1) UNRELEASED; urgency=medium
+golang-go.uber-multierr (1.8.0-1) UNRELEASED; urgency=medium
 
+  [ Nobuhiro Iwamatsu ]
+  * New upstream release.
+
+  [ Debian Janitor ]
   * New upstream release.
 
- -- Nobuhiro Iwamatsu <iwamatsu@debian.org>  Sat, 05 Feb 2022 18:01:10 +0900
+ -- Nobuhiro Iwamatsu <iwamatsu@debian.org>  Tue, 26 Apr 2022 22:02:35 -0000
 
 golang-go.uber-multierr (1.1.0-2) unstable; urgency=medium
 
diff --git a/error.go b/error.go
index 5c9b67d..f45af14 100644
--- a/error.go
+++ b/error.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Uber Technologies, Inc.
+// Copyright (c) 2017-2021 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
@@ -35,8 +35,53 @@
 //
 // 	err = multierr.Append(reader.Close(), writer.Close())
 //
-// This makes it possible to record resource cleanup failures from deferred
-// blocks with the help of named return values.
+// 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:", errors)
+// 	}
+//
+// Appending from a loop
+//
+// You sometimes need to append into an error from a loop.
+//
+// 	var err error
+// 	for _, item := range items {
+// 		err = multierr.Append(err, process(item))
+// 	}
+//
+// Cases like this may require knowledge of whether an individual instance
+// failed. This usually requires introduction of a new variable.
+//
+// 	var err error
+// 	for _, item := range items {
+// 		if perr := process(item); perr != nil {
+// 			log.Warn("skipping item", item)
+// 			err = multierr.Append(err, perr)
+// 		}
+// 	}
+//
+// multierr includes AppendInto to simplify cases like this.
+//
+// 	var err error
+// 	for _, item := range items {
+// 		if multierr.AppendInto(&err, process(item)) {
+// 			log.Warn("skipping item", item)
+// 		}
+// 	}
+//
+// This will append the error into the err variable, and return true if that
+// individual error was non-nil.
+//
+// See AppendInto for more information.
+//
+// Deferred Functions
+//
+// Go makes it possible to modify the return value of a function in a defer
+// block if the function was using named returns. This makes it possible to
+// record resource cleanup failures from deferred blocks.
 //
 // 	func sendRequest(req Request) (err error) {
 // 		conn, err := openConnection()
@@ -49,14 +94,21 @@
 // 		// ...
 // 	}
 //
-// The underlying list of errors for a returned error object may be retrieved
-// with the Errors function.
+// multierr provides the Invoker type and AppendInvoke function to make cases
+// like the above simpler and obviate the need for a closure. The following is
+// roughly equivalent to the example above.
 //
-// 	errors := multierr.Errors(err)
-// 	if len(errors) > 0 {
-// 		fmt.Println("The following errors occurred:", errors)
+// 	func sendRequest(req Request) (err error) {
+// 		conn, err := openConnection()
+// 		if err != nil {
+// 			return err
+// 		}
+// 		defer multierr.AppendInvoke(&err, multierr.Close(conn))
+// 		// ...
 // 	}
 //
+// See AppendInvoke and Invoker for more information.
+//
 // Advanced Usage
 //
 // Errors returned by Combine and Append MAY implement the following
@@ -87,6 +139,7 @@ package multierr // import "go.uber.org/multierr"
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -186,6 +239,33 @@ func (merr *multiError) Errors() []error {
 	return merr.errors
 }
 
+// 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 (merr *multiError) Error() string {
 	if merr == nil {
 		return ""
@@ -292,6 +372,14 @@ func inspect(errors []error) (res inspectResult) {
 
 // fromSlice converts the given list of errors into a single error.
 func fromSlice(errors []error) error {
+	// Don't pay to inspect small slices.
+	switch len(errors) {
+	case 0:
+		return nil
+	case 1:
+		return errors[0]
+	}
+
 	res := inspect(errors)
 	switch res.Count {
 	case 0:
@@ -301,8 +389,13 @@ func fromSlice(errors []error) error {
 		return errors[res.FirstErrorIdx]
 	case len(errors):
 		if !res.ContainsMultiError {
-			// already flat
-			return &multiError{errors: errors}
+			// Error list is flat. Make a copy of it
+			// Otherwise "errors" escapes to the heap
+			// unconditionally for all other cases.
+			// This lets us optimize for the "no errors" case.
+			out := make([]error, len(errors))
+			copy(out, errors)
+			return &multiError{errors: out}
 		}
 	}
 
@@ -421,7 +514,7 @@ func Append(left error, right error) error {
 // 		items = append(items, item)
 // 	}
 //
-// Compare this with a verison that relies solely on Append:
+// Compare this with a version that relies solely on Append:
 //
 // 	var err error
 // 	for line := range lines {
@@ -447,3 +540,113 @@ func AppendInto(into *error, err error) (errored bool) {
 	*into = Append(*into, err)
 	return true
 }
+
+// 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.
+//
+// See also, Close and Invoke.
+type Invoker interface {
+	Invoke() error
+}
+
+// Invoke wraps a function which may fail with an error to match the Invoker
+// interface. Use it to supply functions matching this signature to
+// AppendInvoke.
+//
+// For example,
+//
+// 	func processReader(r io.Reader) (err error) {
+// 		scanner := bufio.NewScanner(r)
+// 		defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))
+// 		for scanner.Scan() {
+// 			// ...
+// 		}
+// 		// ...
+// 	}
+//
+// In this example, the following line will construct the Invoker right away,
+// but defer the invocation of scanner.Err() until the function returns.
+//
+// 	defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))
+type Invoke func() error
+
+// Invoke calls the supplied function and returns its result.
+func (i Invoke) Invoke() error { return i() }
+
+// Close builds an Invoker that closes the provided io.Closer. Use it with
+// AppendInvoke to close io.Closers and append their results into an error.
+//
+// For example,
+//
+// 	func processFile(path string) (err error) {
+// 		f, err := os.Open(path)
+// 		if err != nil {
+// 			return err
+// 		}
+// 		defer multierr.AppendInvoke(&err, multierr.Close(f))
+// 		return processReader(f)
+// 	}
+//
+// In this example, multierr.Close will construct the Invoker right away, but
+// defer the invocation of f.Close until the function returns.
+//
+// 	defer multierr.AppendInvoke(&err, multierr.Close(f))
+func Close(closer io.Closer) Invoker {
+	return Invoke(closer.Close)
+}
+
+// AppendInvoke appends the result of calling the given Invoker into the
+// provided error pointer. Use it with named returns to safely defer
+// invocation of fallible operations until a function returns, and capture the
+// resulting errors.
+//
+// 	func doSomething(...) (err error) {
+// 		// ...
+// 		f, err := openFile(..)
+// 		if err != nil {
+// 			return err
+// 		}
+//
+// 		// multierr will call f.Close() when this function returns and
+// 		// if the operation fails, its append its error into the
+// 		// returned error.
+// 		defer multierr.AppendInvoke(&err, multierr.Close(f))
+//
+// 		scanner := bufio.NewScanner(f)
+// 		// Similarly, this scheduled scanner.Err to be called and
+// 		// inspected when the function returns and append its error
+// 		// into the returned error.
+// 		defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))
+//
+// 		// ...
+// 	}
+//
+// Without defer, AppendInvoke behaves exactly like AppendInto.
+//
+// 	err := // ...
+// 	multierr.AppendInvoke(&err, mutltierr.Invoke(foo))
+//
+// 	// ...is roughly equivalent to...
+//
+// 	err := // ...
+// 	multierr.AppendInto(&err, foo())
+//
+// The advantage of the indirection introduced by Invoker is to make it easy
+// to defer the invocation of a function. Without this indirection, the
+// invoked function will be evaluated at the time of the defer block rather
+// than when the function returns.
+//
+// 	// BAD: This is likely not what the caller intended. This will evaluate
+// 	// foo() right away and append its result into the error when the
+// 	// function returns.
+// 	defer multierr.AppendInto(&err, foo())
+//
+// 	// GOOD: This will defer invocation of foo unutil the function returns.
+// 	defer multierr.AppendInvoke(&err, multierr.Invoke(foo))
+//
+// multierr provides a few Invoker implementations out of the box for
+// convenience. See Invoker for more information.
+func AppendInvoke(into *error, invoker Invoker) {
+	AppendInto(into, invoker.Invoke())
+}
diff --git a/go113_test.go b/error_ext_test.go
similarity index 98%
rename from go113_test.go
rename to error_ext_test.go
index 4fbd0bb..9936e36 100644
--- a/go113_test.go
+++ b/error_ext_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Uber Technologies, Inc.
+// Copyright (c) 2020 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
@@ -18,8 +18,6 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
-// +build go1.13
-
 package multierr_test
 
 import (
diff --git a/error_test.go b/error_test.go
index 96c869e..c053167 100644
--- a/error_test.go
+++ b/error_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Uber Technologies, Inc.
+// Copyright (c) 2017-2021 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
@@ -97,6 +97,12 @@ func TestCombine(t *testing.T) {
 				" -  bar",
 			wantSingleline: "foo; bar",
 		},
+		{
+			giveErrors:     []error{nil, nil, errors.New("great sadness"), nil},
+			wantError:      errors.New("great sadness"),
+			wantMultiline:  "great sadness",
+			wantSingleline: "great sadness",
+		},
 		{
 			giveErrors: []error{
 				errors.New("foo"),
@@ -273,6 +279,14 @@ func TestCombineDoesNotModifySlice(t *testing.T) {
 	assert.Nil(t, errors[1], 3)
 }
 
+func TestCombineGoodCaseNoAlloc(t *testing.T) {
+	errs := make([]error, 10)
+	allocs := testing.AllocsPerRun(100, func() {
+		Combine(errs...)
+	})
+	assert.Equal(t, 0.0, allocs)
+}
+
 func TestAppend(t *testing.T) {
 	tests := []struct {
 		left  error
@@ -555,6 +569,90 @@ func TestAppendInto(t *testing.T) {
 	}
 }
 
+func TestAppendInvoke(t *testing.T) {
+	tests := []struct {
+		desc string
+		into *error
+		give Invoker
+		want error
+	}{
+		{
+			desc: "append into empty",
+			into: new(error),
+			give: Invoke(func() error {
+				return errors.New("foo")
+			}),
+			want: errors.New("foo"),
+		},
+		{
+			desc: "append into non-empty, non-multierr",
+			into: errorPtr(errors.New("foo")),
+			give: Invoke(func() error {
+				return errors.New("bar")
+			}),
+			want: Combine(
+				errors.New("foo"),
+				errors.New("bar"),
+			),
+		},
+		{
+			desc: "append into non-empty multierr",
+			into: errorPtr(Combine(
+				errors.New("foo"),
+				errors.New("bar"),
+			)),
+			give: Invoke(func() error {
+				return errors.New("baz")
+			}),
+			want: Combine(
+				errors.New("foo"),
+				errors.New("bar"),
+				errors.New("baz"),
+			),
+		},
+		{
+			desc: "close/empty",
+			into: new(error),
+			give: Close(newCloserMock(t, errors.New("foo"))),
+			want: errors.New("foo"),
+		},
+		{
+			desc: "close/no fail",
+			into: new(error),
+			give: Close(newCloserMock(t, nil)),
+			want: nil,
+		},
+		{
+			desc: "close/non-empty",
+			into: errorPtr(errors.New("foo")),
+			give: Close(newCloserMock(t, errors.New("bar"))),
+			want: Combine(
+				errors.New("foo"),
+				errors.New("bar"),
+			),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			AppendInvoke(tt.into, tt.give)
+			assert.Equal(t, tt.want, *tt.into)
+		})
+	}
+}
+
+func TestClose(t *testing.T) {
+	t.Run("fail", func(t *testing.T) {
+		give := errors.New("great sadness")
+		got := Close(newCloserMock(t, give)).Invoke()
+		assert.Same(t, give, got)
+	})
+
+	t.Run("success", func(t *testing.T) {
+		got := Close(newCloserMock(t, nil)).Invoke()
+		assert.Nil(t, got)
+	})
+}
+
 func TestAppendIntoNil(t *testing.T) {
 	t.Run("nil pointer panics", func(t *testing.T) {
 		assert.Panics(t, func() {
@@ -580,3 +678,22 @@ func TestAppendIntoNil(t *testing.T) {
 func errorPtr(err error) *error {
 	return &err
 }
+
+type closerMock func() error
+
+func (c closerMock) Close() error {
+	return c()
+}
+
+func newCloserMock(tb testing.TB, err error) io.Closer {
+	var closed bool
+	tb.Cleanup(func() {
+		if !closed {
+			tb.Error("closerMock wasn't closed before test end")
+		}
+	})
+	return closerMock(func() error {
+		closed = true
+		return err
+	})
+}
diff --git a/example_test.go b/example_test.go
index ad7b802..e46a633 100644
--- a/example_test.go
+++ b/example_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2017 Uber Technologies, Inc.
+// Copyright (c) 2017-2021 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
@@ -23,6 +23,7 @@ package multierr_test
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"go.uber.org/multierr"
 )
@@ -92,3 +93,32 @@ func ExampleAppendInto() {
 	// call 3 failed
 	// foo; baz
 }
+
+type fakeCloser func() error
+
+func (f fakeCloser) Close() error {
+	return f()
+}
+
+func FakeCloser(err error) io.Closer {
+	return fakeCloser(func() error {
+		return err
+	})
+}
+
+func ExampleClose() {
+	var err error
+
+	closer := FakeCloser(errors.New("foo"))
+
+	defer func() {
+		fmt.Println(err)
+	}()
+	defer multierr.AppendInvoke(&err, multierr.Close(closer))
+
+	fmt.Println("Hello, World")
+
+	// Output:
+	// Hello, World
+	// foo
+}
diff --git a/go.mod b/go.mod
index ff8bdf9..398d6c9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,9 @@
 module go.uber.org/multierr
 
-go 1.12
+go 1.14
 
 require (
-	github.com/stretchr/testify v1.3.0
+	github.com/stretchr/testify v1.7.0
 	go.uber.org/atomic v1.7.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index ecfc286..75edd73 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,16 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go113.go b/go113.go
deleted file mode 100644
index 264b0ea..0000000
--- a/go113.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) 2019 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.
-
-// +build go1.13
-
-package multierr
-
-import "errors"
-
-// 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
-}
diff --git a/tools/go.mod b/tools/go.mod
index 147f3cf..ebf0180 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -3,8 +3,6 @@ module go.uber.org/multierr/tools
 go 1.12
 
 require (
-	go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee
-	golang.org/x/lint v0.0.0-20190930215403-16217165b5de
-	golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 // indirect
-	honnef.co/go/tools v0.0.1-2019.2.3
+	golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5
+	honnef.co/go/tools v0.1.4
 )
diff --git a/tools/go.sum b/tools/go.sum
index 11e3ae3..fc0f016 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -1,32 +1,35 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ=
+honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
diff --git a/tools/tools.go b/tools/tools.go
index df93f07..fa5465c 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Uber Technologies, Inc.
+// Copyright (c) 2019-2021 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
@@ -18,13 +18,13 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
+//go:build tools
 // +build tools
 
 package multierr
 
 import (
 	// Tools we use during development.
-	_ "go.uber.org/tools/update-license"
 	_ "golang.org/x/lint/golint"
 	_ "honnef.co/go/tools/cmd/staticcheck"
 )

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/go.uber.org/multierr/appendinvoke_example_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/go.uber.org/multierr/error_ext_test.go

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/gocode/src/go.uber.org/multierr/go113.go
-rw-r--r--  root/root   /usr/share/gocode/src/go.uber.org/multierr/go113_test.go

No differences were encountered in the control files

More details

Full run details