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