2019年9月3日にGo 1.13がリリースされました。
リリースノートを見ていて、Error wrappingが気になったので触ってみます。
何はともあれ、ドキュメントを見てみます。 errorsのドキュメントを確認すると、以下4つの関数が存在します。
func As(err error, target interface{}) bool func Is(err, target error) bool func New(text string) error func Unwrap(err error) error
Newはただのコンストラクタなのでいいとして、残り3つは挙動を確認します。
Isを使ってみる
順番が前後しますが、Isから見ていきましょう。
Go1.12以前では、以下のような(if err != nil)エラーハンドリングをしていたと思います。
package main import ( "errors" "fmt" ) var ( MyError = myError() ) func myError() error { return errors.New("myErr") } func simpleError() error { return MyError } func main() { err := simpleError() if err != nil { fmt.Println(err) // myError } }
もしくは、switch文を用いて以下のようにするパターンもありますね。
※以降では、main以外の共通部分は省略します。
func main() { err := simpleError() if err != nil { switch err { case MyError: fmt.Println("MyError:", err) // MyError: myErr default: fmt.Println("default:", err) } } }
Isを用いると以下のように(if errors.Is(err, MyError))なります。
func main() { err := simpleError() if errors.Is(err, MyError) { fmt.Println(err) // myError } }
Isの使い方はわかりましたが、Isの何が嬉しいのでしょうか。
本アップデートの名前にもあるようにwrapしたときに活きてきます。
ドキュメントにあるように、wrapするには%wを使います。
例えば、以下のような実装になります。
func wrappedError() error { err := simpleError() return fmt.Errorf("%w", err) }
実際にwrappedErrorを用いて、wrapしたときのerrの型を見てみましょう。
func main() { err = simpleError() fmt.Printf("%T", err) // *errors.errorString fmt.Println() err := wrappedError() fmt.Printf("%T", err) // *fmt.wrapError }
wrapする前は*errors.errorString型だったものが、*fmt.wrapError型になっていることがわかります。
先ほどのswitch文の例を実行してみると、MyErrorのエラーを捕まえることができなくなってしまいました。
func main() { err := wrappedError() // wrappedErrorに変わったことに注意 if err != nil { switch err { case MyError: fmt.Println("MyError:", err) default: fmt.Println("default:", err) // default: myErr } } }
そこでIsの登場です。
func main() { err := wrappedError() if errors.Is(err, MyError) { fmt.Printf(err.Error()) // myErr } }
こちらはMyErrorを捉えられています。
Asを使ってみる
次にAsについてです。
個人的に最近はパーサーを書くことが多いので、以下のような例を用意しました。
Parseに失敗したときのエラーをハンドリングする例です。
返ってきたerrをInvalidCharに型キャストして、エラーハンドリングしています。
package main import ( "errors" "fmt" ) type InvalidChar struct { // other fields err error } func (ic *InvalidChar) Error() string { ic.err = errors.New("INVALID CHARACTER") return fmt.Errorf("%w", ic.err).Error() } func Parse() error { return &InvalidChar{} } func main() { err := Parse() if ierr, ok := err.(*InvalidChar); ok { fmt.Println(ierr) // INVALID CHARACTER } }
Asを用いると以下のようになります。
func main() { err := Parse() var ierr *InvalidChar if errors.As(err, &ierr) { fmt.Println(ierr) // INVALID CHARACTER } }
ここまでは、Isの例と同じなので本例でもwrappedErrorを実装します。
func wrappedError() error { err := Parse() return fmt.Errorf("%w", err) }
wrapされたerrをGo1.12以前の方法で扱おうとすると以下のようにチェックをすり抜けます。
func main() { err := wrappedError() if ierr, ok := err.(*InvalidChar); ok { fmt.Println(ierr) // 何も出力されない } }
以下のようにAsを用いれば、正しくハンドリングすることができます。
func main() { err := wrappedError() var ierr *InvalidChar if errors.As(err, &ierr) { fmt.Println(ierr) // INVALID CHARACTER } }
Unwrapを触ってみる
最後にUnwrapです。
例はAsで用いたものと同じです。
func main() { err := wrappedError() fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:*fmt.wrapError // Value:INVALID CHARACTER err = errors.Unwrap(err) fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:*main.InvalidChar // Value:INVALID CHARACTER err = errors.Unwrap(err) fmt.Printf("Type:%T\nValue:%v\n", err, err) // Type:<nil> // Value:<nil> }
wrapされたエラーを順番にUnwrapしていくことができ、最後にはnilになります。
所感
- 標準ライブラリや外部ライブラリのエラーをwrapできるようになったのはありがたい
- Go1.13以降で開発されたライブラリを用いることも考慮し、安全に倒すために
Is、Asでエラーハンドリングしたほうがよさそう - エラー型が多くなってきたときにswitch文が使えないのが微妙
3つ目について、Go1.13からは今までのエラーハンドリングが機能しなくなるかもしれない - Qiitaでも言及されています。
以下のようにUnwrapする案も考えましたが、多段でwrapした際にうまく動かなくなるので実用的ではないでしょう。。。
func main() { err := wrappedError() switch errors.Unwrap(err).(type) { case *InvalidChar: fmt.Println("InvalidChar:", err) // InvalidChar: INVALID CHARACTER default: fmt.Println("default:", err) } }