7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

uber-go/multierrで複数のerrorをまとめる

Posted at

multierr

https://github.com/uber-go/multierr
installはgo get -u go.uber.org/multierr

Usage

main.go

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)
	}
}
output
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()

multierr/error.go
// 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

multierr/error.go
type errorGroup interface {
 Errors() []error
}

type multiError struct {
 copyNeeded atomic.Bool
 errors     []error
}

var _ errorGroup = (*multiError)(nil)
go.uber.org/atomic/atomic.go
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[]errorgo.uber.org/atomic.Boolを保持しています。

multierr.fromSlice()

multierr/error.go
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()

multierr/error.go
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.Buffersync.Poolで再利用しています。

まとめ

一つの処理の中で複数のerrorを発生しうる場合や、resouceのclose処理時などで、利用できたらと思います。sync.Poolbytes.Bufferを使いまわすところは、いろいろなところで利用できそうです。multiError.copyNeededとして保持しているatomic.Boolの役割がよく理解できませんでした。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?