Go1.13のError wrappingを触ってみる

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に失敗したときのエラーをハンドリングする例です。 返ってきたerrInvalidCharに型キャストして、エラーハンドリングしています。

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以降で開発されたライブラリを用いることも考慮し、安全に倒すためにIsAsでエラーハンドリングしたほうがよさそう
  • エラー型が多くなってきたときに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)
    }
}

References