mirror of https://github.com/uber-go/multierr.git
Avoid allocating a new slice on every Append (#9)
Currently, appending a non-nil error to a multierror always copies the underlying slice, since there may be multiple goroutines appending to the same multierr,
```go
merr := multierr.Append(errors.New("initial"), errors.New("multierr"))
go func() {
err1 = multierr.Append(merr, errors.New("1"))
[..]
}()
go func() {
err2 = multierr.Append(merr, errors.New("2"))
[..]
}()
```
However, the common use case is a standard for loop:
```go
var merr error
for _, v := range values {
merr = multierr.Append(merr, processValue(v))
}
return merr
```
Since there is only a single resulting slice, we don't need to create a full copy on every `Append`. Instead, we track whether we have modified the underlying slice from `Append` already using an atomic boolean, and only create a copy if the underlying slice has been modified.
This commit is contained in:
parent
737b41aa3b
commit
8deb4d8f84
49
error.go
49
error.go
|
|
@ -27,6 +27,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -68,13 +70,12 @@ var _bufferPool = sync.Pool{
|
||||||
//
|
//
|
||||||
// multiError formats to a semi-colon delimited list of error messages with
|
// multiError formats to a semi-colon delimited list of error messages with
|
||||||
// %v and with a more readable multi-line format with %+v.
|
// %v and with a more readable multi-line format with %+v.
|
||||||
type multiError []error
|
type multiError struct {
|
||||||
|
copyNeeded atomic.Bool
|
||||||
func (merr multiError) String() string {
|
errors []error
|
||||||
return merr.Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (merr multiError) Error() string {
|
func (merr *multiError) Error() string {
|
||||||
buff := _bufferPool.Get().(*bytes.Buffer)
|
buff := _bufferPool.Get().(*bytes.Buffer)
|
||||||
buff.Reset()
|
buff.Reset()
|
||||||
|
|
||||||
|
|
@ -85,7 +86,7 @@ func (merr multiError) Error() string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (merr multiError) Format(f fmt.State, c rune) {
|
func (merr *multiError) Format(f fmt.State, c rune) {
|
||||||
if c == 'v' && f.Flag('+') {
|
if c == 'v' && f.Flag('+') {
|
||||||
merr.writeMultiline(f)
|
merr.writeMultiline(f)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -93,9 +94,9 @@ func (merr multiError) Format(f fmt.State, c rune) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (merr multiError) writeSingleline(w io.Writer) {
|
func (merr *multiError) writeSingleline(w io.Writer) {
|
||||||
first := true
|
first := true
|
||||||
for _, item := range merr {
|
for _, item := range merr.errors {
|
||||||
if first {
|
if first {
|
||||||
first = false
|
first = false
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -105,9 +106,9 @@ func (merr multiError) writeSingleline(w io.Writer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (merr multiError) writeMultiline(w io.Writer) {
|
func (merr *multiError) writeMultiline(w io.Writer) {
|
||||||
w.Write(_multilinePrefix)
|
w.Write(_multilinePrefix)
|
||||||
for _, item := range merr {
|
for _, item := range merr.errors {
|
||||||
w.Write(_multilineSeparator)
|
w.Write(_multilineSeparator)
|
||||||
writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item))
|
writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item))
|
||||||
}
|
}
|
||||||
|
|
@ -164,8 +165,8 @@ func inspect(errors []error) (res inspectResult) {
|
||||||
res.FirstErrorIdx = i
|
res.FirstErrorIdx = i
|
||||||
}
|
}
|
||||||
|
|
||||||
if merr, ok := err.(multiError); ok {
|
if merr, ok := err.(*multiError); ok {
|
||||||
res.Capacity += len(merr)
|
res.Capacity += len(merr.errors)
|
||||||
res.ContainsMultiError = true
|
res.ContainsMultiError = true
|
||||||
} else {
|
} else {
|
||||||
res.Capacity++
|
res.Capacity++
|
||||||
|
|
@ -186,23 +187,24 @@ func fromSlice(errors []error) error {
|
||||||
case len(errors):
|
case len(errors):
|
||||||
if !res.ContainsMultiError {
|
if !res.ContainsMultiError {
|
||||||
// already flat
|
// already flat
|
||||||
return multiError(errors)
|
return &multiError{errors: errors}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
merr := make(multiError, 0, res.Capacity)
|
nonNilErrs := make([]error, 0, res.Capacity)
|
||||||
for _, err := range errors[res.FirstErrorIdx:] {
|
for _, err := range errors[res.FirstErrorIdx:] {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if nested, ok := err.(multiError); ok {
|
if nested, ok := err.(*multiError); ok {
|
||||||
merr = append(merr, nested...)
|
nonNilErrs = append(nonNilErrs, nested.errors...)
|
||||||
} else {
|
} else {
|
||||||
merr = append(merr, err)
|
nonNilErrs = append(nonNilErrs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merr
|
|
||||||
|
return &multiError{errors: nonNilErrs}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine combines the passed errors into a single error.
|
// Combine combines the passed errors into a single error.
|
||||||
|
|
@ -263,10 +265,15 @@ func Append(left error, right error) error {
|
||||||
return left
|
return left
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := right.(multiError); !ok {
|
if _, ok := right.(*multiError); !ok {
|
||||||
if _, ok := left.(multiError); !ok {
|
if l, ok := left.(*multiError); ok && !l.copyNeeded.Swap(true) {
|
||||||
|
// Common case where the error on the left is constantly being
|
||||||
|
// appended to.
|
||||||
|
errs := append(l.errors, right)
|
||||||
|
return &multiError{errors: errs}
|
||||||
|
} else if !ok {
|
||||||
// Both errors are single errors.
|
// Both errors are single errors.
|
||||||
return multiError{left, right}
|
return &multiError{errors: []error{left, right}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
125
error_test.go
125
error_test.go
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -54,6 +55,10 @@ func appendN(initial, err error, n int) error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newMultiErr(errors ...error) error {
|
||||||
|
return &multiError{errors: errors}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCombine(t *testing.T) {
|
func TestCombine(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
giveErrors []error
|
giveErrors []error
|
||||||
|
|
@ -73,15 +78,15 @@ func TestCombine(t *testing.T) {
|
||||||
giveErrors: []error{
|
giveErrors: []error{
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
nil,
|
nil,
|
||||||
multiError{
|
newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar",
|
" - bar",
|
||||||
|
|
@ -90,14 +95,14 @@ func TestCombine(t *testing.T) {
|
||||||
{
|
{
|
||||||
giveErrors: []error{
|
giveErrors: []error{
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
multiError{
|
newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
wantError: newMultiErr(
|
||||||
wantError: multiError{
|
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar",
|
" - bar",
|
||||||
|
|
@ -114,10 +119,10 @@ func TestCombine(t *testing.T) {
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar",
|
" - bar",
|
||||||
|
|
@ -129,11 +134,11 @@ func TestCombine(t *testing.T) {
|
||||||
errors.New("multi\n line\nerror message"),
|
errors.New("multi\n line\nerror message"),
|
||||||
errors.New("single line error message"),
|
errors.New("single line error message"),
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("great sadness"),
|
errors.New("great sadness"),
|
||||||
errors.New("multi\n line\nerror message"),
|
errors.New("multi\n line\nerror message"),
|
||||||
errors.New("single line error message"),
|
errors.New("single line error message"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - great sadness\n" +
|
" - great sadness\n" +
|
||||||
" - multi\n" +
|
" - multi\n" +
|
||||||
|
|
@ -147,18 +152,18 @@ func TestCombine(t *testing.T) {
|
||||||
{
|
{
|
||||||
giveErrors: []error{
|
giveErrors: []error{
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
multiError{
|
newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
errors.New("baz"),
|
errors.New("baz"),
|
||||||
},
|
),
|
||||||
errors.New("qux"),
|
errors.New("qux"),
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
errors.New("baz"),
|
errors.New("baz"),
|
||||||
errors.New("qux"),
|
errors.New("qux"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar\n" +
|
" - bar\n" +
|
||||||
|
|
@ -170,15 +175,15 @@ func TestCombine(t *testing.T) {
|
||||||
giveErrors: []error{
|
giveErrors: []error{
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
nil,
|
nil,
|
||||||
multiError{
|
newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar",
|
" - bar",
|
||||||
|
|
@ -187,14 +192,14 @@ func TestCombine(t *testing.T) {
|
||||||
{
|
{
|
||||||
giveErrors: []error{
|
giveErrors: []error{
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
multiError{
|
newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
wantError: newMultiErr(
|
||||||
wantError: multiError{
|
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - bar",
|
" - bar",
|
||||||
|
|
@ -206,11 +211,11 @@ func TestCombine(t *testing.T) {
|
||||||
richFormatError{},
|
richFormatError{},
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
},
|
||||||
wantError: multiError{
|
wantError: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
richFormatError{},
|
richFormatError{},
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
wantMultiline: "the following errors occurred:\n" +
|
wantMultiline: "the following errors occurred:\n" +
|
||||||
" - foo\n" +
|
" - foo\n" +
|
||||||
" - multiline\n" +
|
" - multiline\n" +
|
||||||
|
|
@ -277,46 +282,46 @@ func TestAppend(t *testing.T) {
|
||||||
{
|
{
|
||||||
left: errors.New("foo"),
|
left: errors.New("foo"),
|
||||||
right: errors.New("bar"),
|
right: errors.New("bar"),
|
||||||
want: multiError{
|
want: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: multiError{
|
left: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
right: errors.New("baz"),
|
right: errors.New("baz"),
|
||||||
want: multiError{
|
want: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
errors.New("baz"),
|
errors.New("baz"),
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: errors.New("baz"),
|
left: errors.New("baz"),
|
||||||
right: multiError{
|
right: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
want: multiError{
|
want: newMultiErr(
|
||||||
errors.New("baz"),
|
errors.New("baz"),
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: multiError{
|
left: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
},
|
),
|
||||||
right: multiError{
|
right: newMultiErr(
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
want: multiError{
|
want: newMultiErr(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,20 +330,40 @@ func TestAppend(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendDoesNotModify(t *testing.T) {
|
func createMultiErrWithCapacity() error {
|
||||||
createInitial := func() error {
|
|
||||||
// Create a multiError that has capacity for more errors so Append will
|
// Create a multiError that has capacity for more errors so Append will
|
||||||
// modify the underlying array that may be shared.
|
// modify the underlying array that may be shared.
|
||||||
return appendN(nil, errors.New("initial"), 50)
|
return appendN(nil, errors.New("append"), 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
initial := createInitial()
|
func TestAppendDoesNotModify(t *testing.T) {
|
||||||
|
initial := createMultiErrWithCapacity()
|
||||||
err1 := Append(initial, errors.New("err1"))
|
err1 := Append(initial, errors.New("err1"))
|
||||||
err2 := Append(initial, errors.New("err2"))
|
err2 := Append(initial, errors.New("err2"))
|
||||||
|
|
||||||
// initial should not have been modified.
|
// Make sure the error messages match, since we do modify the copyNeeded
|
||||||
assert.Equal(t, createInitial(), initial, "Initial should not be modified")
|
// atomic, the values cannot be compared.
|
||||||
|
assert.EqualError(t, initial, createMultiErrWithCapacity().Error(), "Initial should not be modified")
|
||||||
|
|
||||||
assert.Equal(t, Append(createInitial(), errors.New("err1")), err1)
|
assert.EqualError(t, err1, Append(createMultiErrWithCapacity(), errors.New("err1")).Error())
|
||||||
assert.Equal(t, Append(createInitial(), errors.New("err2")), err2)
|
assert.EqualError(t, err2, Append(createMultiErrWithCapacity(), errors.New("err2")).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendRace(t *testing.T) {
|
||||||
|
initial := createMultiErrWithCapacity()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
err := initial
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
err = Append(err, errors.New("err"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
hash: cb430176250bcfe8bafd93aa48d285fa35b08af59b5f9971dcf553a1d6c236ec
|
hash: b53b5e9a84b9cb3cc4b2d0499e23da2feca1eec318ce9bb717ecf35bf24bf221
|
||||||
updated: 2017-03-17T16:22:25.024129734-07:00
|
updated: 2017-04-10T13:34:45.671678062-07:00
|
||||||
imports: []
|
imports:
|
||||||
|
- name: go.uber.org/atomic
|
||||||
|
version: 3b8db5e93c4c02efbc313e17b2e796b0914a01fb
|
||||||
testImports:
|
testImports:
|
||||||
- name: github.com/davecgh/go-spew
|
- name: github.com/davecgh/go-spew
|
||||||
version: 346938d642f2ec3594ed81d874461961cd0faa76
|
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
|
||||||
subpackages:
|
subpackages:
|
||||||
- spew
|
- spew
|
||||||
- name: github.com/pmezard/go-difflib
|
- name: github.com/pmezard/go-difflib
|
||||||
version: 792786c7400a136282c1664665ae0a8db921c6c2
|
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||||
subpackages:
|
subpackages:
|
||||||
- difflib
|
- difflib
|
||||||
- name: github.com/stretchr/testify
|
- name: github.com/stretchr/testify
|
||||||
version: 4d4bfba8f1d1027c4fdbe371823030df51419987
|
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
|
||||||
subpackages:
|
subpackages:
|
||||||
- assert
|
- assert
|
||||||
|
- require
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package: go.uber.org/multierr
|
package: go.uber.org/multierr
|
||||||
import: []
|
import:
|
||||||
|
- package: go.uber.org/atomic
|
||||||
|
version: ^1
|
||||||
testImport:
|
testImport:
|
||||||
- package: github.com/stretchr/testify
|
- package: github.com/stretchr/testify
|
||||||
subpackages:
|
subpackages:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue