KDOC 96: errors.As()の使い方

この文書のステータス

  • 作成
    • 2024-02-17 貴島
  • レビュー
    • 2024-02-25 貴島

概要

以前Goのerrors.Is()を調べたので、今度はerrors.As()を調べる。違いは何だろうか。

func As(err error, target any) bool

ユースケース

まずユースケースを見る。fs.PathErrorはUnwrap()、As()を実装してないので、エラーツリーを辿らず独自判定ロジックで実行しない。

if _, err := os.Open("non-existing"); err != nil {
        var pathError *fs.PathError
        if errors.As(err, &pathError) {
                fmt.Println("Failed at path:", pathError.Path)
        } else {
                fmt.Println(err)
        }
}
Failed at path: non-existing

errをpathErrorに代入しているように見える。それによって、変数pathErrorのフィールドに値が入って、使えるようになっている。エラーの中身を使いたいとき用ということか。

直接取り出せばいいのでは、と考えたがerr.Path errはos.Open()が返したインターフェース型 error なので、まだ型が未確定。なのでerr.PathはUndefinedになる。型アサーションしてからだとフィールドへアクセスできるようになる。現在コードの状況に限定して、同じようなコードを型アサーションを使って書いた。

if _, err := os.Open("non-existing"); err != nil {
        var pathError *fs.PathError
        pathError, _ = err.(*fs.PathError)
        if pathError != nil {
                fmt.Println("Failed at path:", pathError.Path)
        } else {
                fmt.Println(err)
        }
}
Failed at path: non-existing

↑エラーツリーを辿らない、入力が限定されたもっともシンプルなケースでは、型アサーションでも同じ意味に見える。ただ、見やすさはよくなさそうだ。errors.As()は型アサーションと結果ブーリンアンの返却をシンプルにやってくれる関数、といえそうだ。

errors.Is()との違いは、代入を使っていることだ。errの中身を取り出すには、本質的にerrorインターフェースの型を確定して、ターゲットの型を持つ別な変数に代入する必要がある。errors.Is()では == を使って多くの場合ポインタによって、つまりメモリアドレスでエラーの種類を判断していた。したがってIs()で型は全く出てこなかった。As()では代入が関連するので型が重要そうに見える。

↓実装を見てみよう。本質的な部分はこのあたりか。

if reflectlite.TypeOf(err).AssignableTo(targetType) {
	targetVal.Elem().Set(reflectlite.ValueOf(err))
	return true
}

素朴に、err(Asの第1引数)の型がtarget(Asの第2引数)の型へ代入可能であれば、targetの値にerrの値をセットする、に見える。

では代入可能とはなにか、なぜ代入時に見慣れない書き方をしているのか、ということになるのだが、深遠なテーマになりそうなのでここでは進まない。

まとめ

Is()とAs()の違いをまとめる。

  • errors.Is()は単純にエラーを区別する関数。同値性によって判定。どうやって同値とするかは型によって異なるがおおむねポインタ型によるメモリアドレス判定が使われる
  • errors.As()はエラーを区別しつつ、型アサーションする関数。型の代入可能性によって判定。エラーのフィールドを利用したいときに使う

関連