multierr
https://github.com/uber-go/multierr
installはgo get -u go.uber.org/multierr
Usage
import (
"io"
"os"
"go.uber.org/multierr"
)
func open(paths []string) ([]io.WriteCloser, func(), error) {
var openErr error
files := make([]io.WriteCloser, 0, len(paths))
close := func() {
for _, f := range files {
f.Close()
}
}
for _, path := range paths {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
openErr = multierr.Append(openErr, err)
if err == nil {
files = append(files, f)
}
}
return files, close, openErr
}
func main() {
paths := []string{"./nonexists/test1.txt", "./nonexists/test2.txt", "./nonexists/test3.txt"}
_, close, err := open(paths)
defer close()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n\n", err)
errs := multierr.Errors(err)
for _, openErr := range errs {
fmt.Fprintln(os.Stderr, openErr)
}
fmt.Fprintf(os.Stderr, "\n%+v\n", err)
}
}
open ./nonexists/test1.txt: no such file or directory; open ./nonexists/test2.txt: no such file or directory; open ./nonexists/test3.txt: no such file or directory
open ./nonexists/test1.txt: no such file or directory
open ./nonexists/test2.txt: no such file or directory
open ./nonexists/test3.txt: no such file or directory
the following errors occurred:
- open ./nonexists/test1.txt: no such file or directory
- open ./nonexists/test2.txt: no such file or directory
- open ./nonexists/test3.txt: no such file or directory
-
multierr.Append()
でerrorを追加 -
multierr.Errors()
にてerror sliceに変換 -
%+v
でformatすると, multilineで出力される
Walkthrough
multierr.Append()
// Append appends the given errors together. Either value may be nil.
//
// This function is a specialization of Combine for the common case where
// there are only two errors.
//
// err = multierr.Append(reader.Close(), writer.Close())
//
// The following pattern may also be used to record failure of deferred
// operations without losing information about the original error.
//
// func doSomething(..) (err error) {
// f := acquireResource()
// defer func() {
// err = multierr.Append(err, f.Close())
// }()
func Append(left error, right error) error {
switch {
case left == nil:
return right
case right == nil:
return left
}
if _, ok := right.(*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.
return &multiError{errors: []error{left, right}}
}
}
// Either right or both, left and right, are multiErrors. Rely on usual
// expensive logic.
errors := [2]error{left, right}
return fromSlice(errors[0:])
}
multierr.Append()
は, 与えられた2つのerrorに応じて、そのままどちらかを返すか、multiError
を作成して返します。
!l.copyNeeded.Swap(true)
の条件によって、同じmultiErrorに対する2回目以降のappendが実行されないようにされています。
errors
を[2]error{left,right}
で初期化して、errors[0:]
によってsliceとしてfromSlice()
に渡します。最初から[]error{left,rigth}
としていない理由はわかりません。
multierr.multiError
type errorGroup interface {
Errors() []error
}
type multiError struct {
copyNeeded atomic.Bool
errors []error
}
var _ errorGroup = (*multiError)(nil)
import (
"math"
"sync/atomic"
)
// Bool is an atomic Boolean.
type Bool struct{ v uint32 }
// Swap sets the given value and returns the previous value.
func (b *Bool) Swap(new bool) bool {
return truthy(atomic.SwapUint32(&b.v, boolToInt(new)))
}
func truthy(n uint32) bool {
return n&1 == 1
}
func boolToInt(b bool) uint32 {
if b {
return 1
}
return 0
}
multierr.multiError
は[]error
とgo.uber.org/atomic.Bool
を保持しています。
multierr.fromSlice()
type inspectResult struct {
// Number of top-level non-nil errors
Count int
// Total number of errors including multiErrors
Capacity int
// Index of the first non-nil error in the list. Value is meaningless if
// Count is zero.
FirstErrorIdx int
// Whether the list contains at least one multiError
ContainsMultiError bool
}
// Inspects the given slice of errors so that we can efficiently allocate
// space for it.
func inspect(errors []error) (res inspectResult) {
first := true
for i, err := range errors {
if err == nil {
continue
}
res.Count++
if first {
first = false
res.FirstErrorIdx = i
}
if merr, ok := err.(*multiError); ok {
res.Capacity += len(merr.errors)
res.ContainsMultiError = true
} else {
res.Capacity++
}
}
return
}
// fromSlice converts the given list of errors into a single error.
func fromSlice(errors []error) error {
res := inspect(errors)
switch res.Count {
case 0:
return nil
case 1:
// only one non-nil entry
return errors[res.FirstErrorIdx]
case len(errors):
if !res.ContainsMultiError {
// already flat
return &multiError{errors: errors}
}
}
nonNilErrs := make([]error, 0, res.Capacity)
for _, err := range errors[res.FirstErrorIdx:] {
if err == nil {
continue
}
if nested, ok := err.(*multiError); ok {
nonNilErrs = append(nonNilErrs, nested.errors...)
} else {
nonNilErrs = append(nonNilErrs, err)
}
}
return &multiError{errors: nonNilErrs}
}
fromSlice()
はinspect()
によって、与えられた[]error
の中にerror
, nil
, multiError
があるかを調べます。
そして、nil
は無視し、multiError
はflatして保持します。
multiError.Error()
func (merr *multiError) Error() string {
if merr == nil {
return ""
}
buff := _bufferPool.Get().(*bytes.Buffer)
buff.Reset()
merr.writeSingleline(buff)
result := buff.String()
_bufferPool.Put(buff)
return result
}
// _bufferPool is a pool of bytes.Buffers.
var _bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var (
// Separator for single-line error messages.
_singlelineSeparator = []byte("; ")
)
func (merr *multiError) writeSingleline(w io.Writer) {
first := true
for _, item := range merr.errors {
if first {
first = false
} else {
w.Write(_singlelineSeparator)
}
io.WriteString(w, item.Error())
}
}
multiError.Error()
は内部に保持しているerror
をseparator(;
)で結合して出力します。出力の際に使用するbytes.Buffer
はsync.Pool
で再利用しています。
まとめ
一つの処理の中で複数のerrorを発生しうる場合や、resouceのclose処理時などで、利用できたらと思います。sync.Pool
でbytes.Buffer
を使いまわすところは、いろいろなところで利用できそうです。multiError.copyNeeded
として保持しているatomic.Bool
の役割がよく理解できませんでした。