github.com/k1LoW/errors に errors.Join でまとめられた error を分割する Errors 関数を追加した

私はZero dependencyな error パッケージ github.com/k1LoW/errors をメンテナンスしています。

github.com

以前書いたエントリはこちら。

k1low.hatenablog.com

今回、github.com/k1LoW/errors に errors.Join でまとめられた error[]error に分割できる errors.Errors 関数を追加しました。

なぜ Errors 関数を追加したか

Go 1.20 で errors.Join が追加されました。複数のエラーを1つにまとめることができる便利な関数です。

err := errors.Join(errA, errB, errC)

しかし、まとめられたエラーを取り出す方法は用意されていません。

errors.Join が返すエラーは Unwrap() []error メソッドを実装しているので自分で取り出すことはできますが、ネストしたケースに対応しようとすると少し面倒です。

// これはネストしていない場合のみ
if je, ok := err.(interface{ Unwrap() []error }); ok {
    errs := je.Unwrap()
    // ...
}

そこで、再帰的にすべての error を取り出してフラットな []error として返す errors.Errors 関数を追加しました。

使い方

使い方はシンプルです。

import "github.com/k1LoW/errors"

errA := errors.New("error a")
errB := errors.New("error b")
errC := errors.New("error c")

// errors.Join でまとめる
joined := errors.Join(errA, errors.Join(errB, errC))

// errors.Errors で分割
errs := errors.Errors(joined)
// errs[0] == errA
// errs[1] == errB
// errs[2] == errC

ネストした errors.Join にも対応しており、すべてのエラーをフラットに取り出せます。

errors.Join されていない普通のエラーを渡した場合は、そのエラーだけを含む []error を返します。

err := errors.New("single error")
errs := errors.Errors(err)
// errs[0] == err
// len(errs) == 1

実装

実装もシンプルです。

// Errors returns all joined errors in the given error.
func Errors(err error) []error {
  je, ok := err.(joinError)
  if !ok {
    return []error{err}
  }
  errs := je.Unwrap()
  var splitted []error
  for _, e := range errs {
    splitted = append(splitted, Errors(e)...)
  }
  return splitted
}

Unwrap() []error を持つエラーかどうかを判定し、持っていれば再帰的に各エラーを展開していきます。持っていなければそのまま返します。

ユースケース

どういうときに使うかというと、例えば複数のエラーをまとめて返す処理があり、それを受け取った側でエラーごとに異なる処理をしたい場合などです。

// 複数のバリデーションエラーをまとめて返す
func validate(input Input) error {
    var errs []error
    if input.Name == "" {
        errs = append(errs, &ValidationError{Field: "name", Message: "required"})
    }
    if input.Age < 0 {
        errs = append(errs, &ValidationError{Field: "age", Message: "must be positive"})
    }
    return errors.Join(errs...)
}

// 受け取った側でエラーごとに処理
err := validate(input)
if err != nil {
    for _, e := range errors.Errors(err) {
        var ve *ValidationError
        if errors.As(e, &ve) {
            // フィールドごとにエラーメッセージを表示
            fmt.Printf("%s: %s\n", ve.Field, ve.Message)
        }
    }
}

go.uber.org/multierrgithub.com/hashicorp/go-multierror も同じようなエラーの分割する機能はありますが、github.com/k1LoW/errors はZero dependencyですし、標準のerrorsパッケージと互換があり、(github.com/hashicorp/go-multierrorでは必要な)型アサーションも必要ありません。

まとめ

github.com/k1LoW/errors パッケージに errors.Errors 関数を追加しました。

依然としてZero dependencyなので使い勝手は良いのではないかと思います。

是非採用のご検討のほどよろしくお願いいたします。