注:本記事は元々 Qiita で公開していたものの再掲です。
Go 1.13 で追加された、エラーラッピングについて。
- Package errors (golang.org)
概要
- エラーを他のエラーでラップすることが可能になる
- 元のエラーの型やフィールド値を保持しつつ新しいエラーを生成できる
- ラップされたエラーは、あとから取り出すことが可能
- 呼び出し先の深い場所で起きたエラーの種類を判別しやすくなる
使い方
エラーをラップする
innerErr := fmt.Errorf("Inner Error") newErr := fmt.Errorf("Wrapped: %w", innerErr)
従来使用してきた fmt.Errorf()
でできる。
通常は fmt.Errorf()
は errors.errorString
側のエラーを返すが、以下の条件を満たす場合、fmt.wrapError
型のエラーを返す。
- フォーマット指定子に
%w
を使用する。文字列内での位置は問わない。 - その
%w
を置き換えるのがerror
型である(上のinnerErr
)
fmt.wrapError
とは
ラップ機能が追加された error
型。
通常 errors.New()
が返す errorString
型は、エラー内容を単純な文字列として保持している。
この方法でエラーをネストさせる場合、エラー文字列を連結していくという方法をとらざるを得ない。結果的に以下のようになると思う。
err := fmt.Errorf("Inner Error") newErr := fmt.Errorf("Wrapped: %s", err) fmt.Println(newErr) // 'Wrapped: Inner Error'
これだと、エラーの種類の判別がやや面倒になってしまう。(上記で言うと、Inner Error
の発生がエラーの根本原因になっているかどうかを判定する場面など。文字列のパースが必要になる。)
しかし fmt.wrapError
は以下の仕組みを持っているため、元のエラーを単独で管理することが可能となる。
fmt.wrapError
構造体には、元のエラーを保持するフィールドがある(err
)
type wrapError struct { msg string err error // ここに元のエラーが入る }
Unwrap()
を実装している。これはラップされた元のエラーを返す。
func (e *wrapError) Unwrap() error { return e.err }
このため、アンラップ時に元のエラーを取り出すことが簡単になっている
エラーをアンラップする
あるエラーからラップされた中身のエラーを取り出すには、errors.Unwrap()
を使う。
errors.Unwrap(newErr error) error
例
iErr := fmt.Errorf("Inner Error") newErr := fmt.Errorf("Wrapped: %w", iErr) // "Wrapped: Inner Error" innerErr := errors.Unwrap(newErr) // "Inner Error"
Unwrap()
の引数で渡されたエラーがUnwrap()
を実装していれば、その戻り値を返すnewErr
はfmt.wrapError
型。これはUnwrap()
を実装していて、newErr.err
=iErr
を返す。
Unwrap()
が未実装ならnil
を返す
エラーを探索する
ラップされた一連のエラーの中から、目的のエラーを探し出すことができる。以下の2つのメソッドが提供されている。
errors.Is(err, target error)
errors.As(err error, target interface{}
errors.Is
: 特定のエラーを探す
- エラーチェーンの中に、特定のエラーが存在するかどうかを調べる。存在すると判定される条件は以下のいずれか。
err == target
が成立する err がエラーチェーンに存在するerr
でIs()
が実装されていて、err.Is(target)
がtrue
を返す err がエラーチェーンに存在する
- 例えば
A
->B
->C
の順にラップしたとして、A
をラップしているかどうかを判定できる。
err == target
が成立する err がエラーチェーンに存在する例
https://play.golang.org/p/GoL2silcyCF
func main() { errA := fmt.Errorf("First Error") // これをラップしているかどうかを判定したい errB := fmt.Errorf("Wrapped: %w", errA) // B <- A fmt.Printf("errB wraps errA = %v\n", errors.Is(errB, errA)) errC := fmt.Errorf("Wrapped: %w", errB) // C <- B <- A fmt.Printf("errC wraps errA = %v\n", errors.Is(errC, errA)) errX := fmt.Errorf("Another error") // A をラップしてない fmt.Printf("errX wraps errA = %v\n", errors.Is(errX, errA)) }
実行結果
errB wraps errA = true errC wraps errA = true errX wraps errA = false
errA == errB
が成立するためには、同一インスタンスである必要がある。以下の例で、エラー文字列が同一で型も同一のインスタンスを2つ作成しているが、別インスタンスの場合は同一と判定されない。文字列が同じだから同一、ということではないので注意。
https://play.golang.org/p/X71IvCmYi6s
errA := fmt.Errorf("Error Y") errB := fmt.Errorf("Error Y") // errA と全く同じ文字列 errX := fmt.Errorf("Wrap: %w", errA) fmt.Printf("errX wraps errA : %v\n", errors.Is(errX, errA)) fmt.Printf("errX wraps errB : %v\n", errors.Is(errX, errB))
実行結果
errX wraps errA : true errX wraps errB : false
なので通常は、グローバル空間でエラーオブジェクトを生成しておき、それを使い回すという方法で比較することになると思う。
err.Is()
が実装されていて、true
を返す例
2つの err が通常なら等価と判断されない場合であっても、任意の条件で同一とみなしたい場合は、Is()
を実装すると良い。
以下の例では、errA
と errB
が同一インスタンスどころか同一の型ですらないが、結果的に同一のエラーと判定させている。
https://play.golang.org/p/sZtPmqjg7Ms
type myError struct { error } // 文字列が一致していれば同一と判断 func (e myError) Is(target error) bool { return e.Error() == target.Error() } func main() { errA := &myError{fmt.Errorf("Error E")} errB := fmt.Errorf("Error E") errX := fmt.Errorf("Wrapped %w", errA) fmt.Printf("errX wraps errB : %v\n", errors.Is(errX, errB)) }
実行結果
errX wraps errB : true
errX
は errA
をラップしている。errA
と errB
は型が異なるが、Is()
で等価と判断されている。
結果 errX
は errB
に相当するものをラップしている、とみなされる。
errors.Is()
は、特定のエラーがエラーチェーン内にあるかどうかを判定するだけ。その特定のエラーを取得し、さらになんらかの形で利用したい場合は、この次の errors.As
を使用すると良い。
errors.As
: 特定の型を持つエラーが含まれるか判定し、それを取得する
errors.As(err error, target interface{})
を使う。
エラーチェーンを辿っていき、target
に代入可能なエラーが見つかったらそれを target
で受け取る。
戻り値は bool
で、目的のエラーが見つかった場合に true
、見つからなければ false
が返る。
エラーに時刻などの付加情報を追加し、それを利用したい場合はこちらが便利。
(代入可能の詳細は こちら )
target
は以下のいずれかを指定可能。
- Interface 型へのポインタ
error
型を実装した型へのポインタ
error
型を実装した独自のエラー型を使用した例。
https://play.golang.org/p/fr_jLSk6gd_z
type myError struct { t time.Time } func (e myError) Error() string { return fmt.Sprintf("[%s] myError found!", e.t.Format("2006/1/2 15:04:05")) } func main() { var innerErr myError err := wrap1() if errors.As(err, &innerErr) { fmt.Printf("%s\n", innerErr) } else { fmt.Printf("Unknown error : %s\n", err) } } func wrap1() error { err := wrap2() return fmt.Errorf("wrap1: %w", err) } func wrap2() error { return myError{t: time.Now()} }
余談
fmt.Errorf
で %w
を2つ以上指定するとどうなるか
(追記: 2022/12/07)
go1.20 から、2つ以上のエラーのラッピングが可能となるようです。
https://github.com/golang/go/issues/53435 https://twitter.com/inancgumus/status/1597571791941414912
以下の記述は、それ以前のエラーラッピングに関するものです。
2つ以上のエラーを同時にラップしようとするとどうなるのか。結論から言うとラップできず通常の errors.errorString
が返ってくる。当然 Unwrap()
でラップしたエラーを取り出すこともできなくなる。
https://play.golang.org/p/NqfHjIzApCQ
errA := fmt.Errorf("Error A") errB := fmt.Errorf("Error B") errC := fmt.Errorf("Error C") errX := fmt.Errorf("1: %w, 2: %w, 3: %w", errA, errB, errC) fmt.Printf("%#v\n", errX) errU := errors.Unwrap(errX) fmt.Println(errU)
実行結果
&errors.errorString{s:"1: Error A, 2: %!w(*errors.errorString=&{Error B}), 3: %!w(*errors.errorString=&{Error C})"} <nil>
ラップしたエラーの型がもはや fmt.wrapError
ではなく errors.errorString
になっている。
そもそも複数のエラーを指定することは意図的に考慮の対象外となっている模様。うっかりエラーを見落とすなど想定外の挙動に繋がりかねないので注意する必要がある。
// It is invalid to use %w other than with Errorf, more than once, or with a non-error arg.