Go
2
どのような問題がありますか?

この記事は最終更新日から3年以上が経過しています。

投稿日

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

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の役割がよく理解できませんでした。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ymgyt

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
2
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー