KDOC 96: errors.As()の使い方
この文書のステータス
- 作成
- 2024-02-17 貴島
- レビュー
- 2024-02-25 貴島
ユースケース
まずユースケースを見る。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()では代入が関連するので型が重要そうに見える。
↓実装を見てみよう。本質的な部分はこのあたりか。
https://github.com/kd-collective/go/blob/b8ac61e6e64c92f23d8cf868a92a70d13e20a124/src/errors/wrap.go#L118-L121
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()はエラーを区別しつつ、型アサーションする関数。型の代入可能性によって判定。エラーのフィールドを利用したいときに使う
関連
- KDOC 86: errors.Is()の比較ロジック。IsとAsがあるのはなぜか疑問を持ったので調べた