Goのerrors.Is(),errors.As()を知るために

「新卒Gopherくん」
 Powered by Gopherize.me - A Gopher pic that's as unique as you
The Gopher character is based on the Go mascot designed by Renée French 

はじめに

こんにちは!!!!ライクル事業部エンジニアの黒田(@knkurokuro7)です。
僕は今年の4月にSO Techonologies株式会社に新卒入社し、5月にライクル事業部に配属されました。

学生の頃からGo が好きなのですが、ありがたいことにライクル事業部ではバックエンドの開発には Go を中心に使用しています。
そのため、毎日のエンジニア業務が楽しすぎます〜!

ただ、まだまだ新卒エンジニアで未熟者なので、日常業務の中で疑問がたくさん湧いてきて、周りのエンジニアの方に手取り足取りご教授いただいております。(優しい方ばかりなので、とっても質問しやすい環境です笑)

今回は、Goで開発している中で、疑問に思い、調べたことについて書こうと思います!

errors.Is(), errors.As()について

業務の中で、Go でエラーの種類によってslackの通知を出し分けたい!ということがありました。
そんな時に、既存コードを眺めていると、

if err != nil {
        if errors.Is(err, sql.ErrHoge) {
            return nil, ErrHoge
        }
        return nil, err
    }

if err != nil {
        var verrs ValidationError
        if errors.As(err, &verr) {
            return nil, ErrHoge
        }
        return nil, err
    }

のような形で、エラーを判定している部分がありました。
「なるほど、こういう形でエラーを判定して、出し分けたらいいんだな」と思い、errors.Is()とerrors.As()というものについて調べてみることにしました。

はじめに、errors.Is()とerrors.As()の概要を知るために、公式ドキュメントを読んでみることにしました。

それぞれの記述の部分を読むと、
errors.Is()

Is reports whether any error in err's chain matches target.

翻訳

Isはerrのチェーンの中にあるエラーがターゲットに一致するかどうかを報告する。

errors.As()

As finds the first error in err's chain that matches target, and if one is found, sets target to that error value and returns true. Otherwise, it returns false.

翻訳

As は err のチェーンの中で target に一致する最初のエラーを見つけ、見つかった場合は、 target にそのエラー値をセットし、true を返す。そうでなければ,falseを返す。

ここまでだと、ほぼ同じような使い方だと思っていました。。。 そこで、もっと読み進めていくと

errors.Is()

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap. An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true.

翻訳

このチェーンは、err 自身と、Unwrap を繰り返し呼び出すことで得られる一連のエラーから構成される。 エラーは、ターゲットと等しいか、または Is(target) がtrueを返すようなメソッド Is(error) bool を実装している場合、ターゲットに一致するとみなされる。

errors.As()

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap. An error matches target if the error's concrete value is assignable to the value pointed to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target.

翻訳

このチェーンは、err 自身と、Unwrap を繰り返し呼び出すことで得られる一連のエラーから構成される。 エラーの具体的な値が target が指す値に代入可能である場合、あるいは、 エラーが As(target) がtrueを返すような As(interface{}) bool メソッドを持っている場合、 エラーは target に一致する。後者の場合、Asメソッドがtargetをセットすることに責任を持つ。

つまり、

  • errors.Is(err, target)はerrがtargetとするものと等しいかどうか判定するもの
  • errors.As(err, target)はerrがtargetとするものに代入可能かどうか判定するもの
  • また、errors.Is(err, target)errors.As(err, target)はそれぞれ、それ自身がtrueを返すようなIs(),As()メソッドを持っている場合にはtargetに一致する。

なのかなとなんとなく理解していました。

内部実装

さて、曖昧な理解だと、どのような場合に使用すべきかというのがわからないなぁと思い、errors.Is()errors.As()の実装を見てみることにしました。

errors.Is()

errors.Is()のソースコード

func Is(err, target error) bool {
    if target == nil {
        return err == target
    }              // ・・・1

    isComparable := reflectlite.TypeOf(target).Comparable()     // ・・・2
    for {        // ・・・3
        if isComparable && err == target {    // ・・・4
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {   // ・・・5
            return true
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}
  1. まず、 if target == nil { return err == target }
    この部分は、targetがnilの場合は、errもnilだとtrueを返し、nilでないとfalseを返すようになっています。

  2. isComparable := reflectlite.TypeOf(target).Comparable()
    この部分は、読んでそのままですが、targetの型が比較可能かどうか判定しています。

  3. そして forの中の部分ですが、主に、errがnilでない限り、Unwrap()し続けます。
    Unwrap()メソッドも、errorsパッケージに定義されています。
    詳細は省きますが、ラップされているエラーを文字通りアンラップするというメソッドです!

  4. if isComparable && err == targetで比較可能かつ、errとtarget が一致した時、trueを返します。

  5. そして、
    if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target)の部分です。
    ここは、ちょっとややこしいのですが、
    t, ok := i.(T)の形で、型アサーションをしていて、
    errがIs()メソッドを実装していれば、 x は実際の値が代入され、 ok はtrueになります。
    そして、x.Is(target)で、x自身に実装されているIs()メソッドを使い、一致した場合は結果を返します。

errors.As()

errors.As()のソースコード

func As(err error, target any) bool {
    if target == nil {
        panic("errors: target cannot be nil")
    }   // ・・・1
    val := reflectlite.ValueOf(target) // ・・・2
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    targetType := typ.Elem() // ・・・3
    if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    for err != nil { // ・・・4
        if reflectlite.TypeOf(err).AssignableTo(targetType) { // ・・・5
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) { // ・・・6
            return true
        }
        err = Unwrap(err)
    }
    return false
}

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

errors.As()errors.Is()より少し複雑です。

  1. まず、 if target == nil { panic("errors: target cannot be nil") }の部分で先ほどと違い、targetがnilの場合はpanicを起こしてしまいます。

  2. そして、 val := reflectlite.ValueOf(target) typ := val.Type() if typ.Kind() != reflectlite.Ptr || val.IsNil() { panic("errors: target must be a non-nil pointer") }
    の部分では、targetの型がポインタ型ではない、もしくは値がnilの場合はpanicを起こしてしまいます。

  3. 次に、 targetType := typ.Elem() if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) { panic("errors: *target must be interface or implement error") }
    では、まずtyp.Elem()の部分は、こちらのコメントにあるように

    Elem returns a type's element type. It panics if the type's Kind is not Ptr.

    要素の型を返して、その型がポインタではない場合はpanicを起こしてしまいます。

    そして以下の部分で、 if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) { panic("errors: *target must be interface or implement error") }
    そのtargetの型がインターフェース型ではないかつ、errorを実装していない場合はpanicを起こしてしまいます。
    errorTypevar errorType = reflectlite.TypeOf((*error)(nil)).Elem()として定義されています。

  4. for err != nilの中では、errがnilでない限り、errを Unwrap()し続けます。

  5. そして、if reflectlite.TypeOf(err).AssignableTo(targetType)でerrが先ほどのtargetの型に代入可能な場合は、val.Elem().Set(reflectlite.ValueOf(err))でtargetにerrをセットし、trueを返します。

  6. また、errors.Is()と同じように、if x, ok := err.(interface{ As(any) bool }); ok && x.As(target)でinterface{ As(any) bool }で型アサーションをして、x自身に実装されているAs()メソッドで評価し、一致した場合は結果を返します。
    また、forを抜けた際には、falseを返します。

ここまでのまとめ

長々と書きましたが、まとめると
errors.Is(err, target)

  • targetがnilでも比較可能。
  • errがIs()メソッドを持っているかつそのメソッド自身がtrueを返す場合には、trueを返す。
  • errをUnwrap()し続けて、err == targetの場合にtrueを返す。

errors.As(err, target)

  • targetがnilや、ポインタ型でない場合だとpanicを起こす。
  • errがAs()メソッドを持っているかつそのメソッド自身がtrueを返す場合には、trueを返す。
  • errをUnwrap()し続けて、targetに代入可能の型と判定された場合にtrueを返す。
  • targetにerrをセットする。

という感じです。
ドキュメントを読んだ時よりも、具体的にどういう場合にtrueを返して、どういう場合にpanicになるのかというのがわかったことで、理解が深まりました。

それぞれの使い分け

さて、問題はどう使い分けるのかということです。 これまでのことを踏まえるとerrors.Is()errors.As()は、

errors.Is(err, target)

  • ラップされたエラーでも、targetとなるエラーと一致するかどうか、値として判定したい時。

errors.As(err, target)

  • ラップされたエラーでも、targetとなるエラーに代入可能かどうか、型として判定したい時。

また、errors.As()は、型を比較して、targetにerrをセットするために、ポインタ型のでnilではない値を渡すことに注意しないといけないなと思いました!

最初に眺めていたerrors.As()のコードは以下のようになっていました。

if err != nil {
        var verrs ValidationError
        if errors.As(err, &verr) {
            return nil, ErrHoge
        }
        return nil, err
    }

ValidationErrorが定義されている部分は、

type ValidationError InvalidError

func (v ValidationError) Error() string {
    b, err := json.Marshal(v)
    if err != nil {
        return "hogehoge error"
    }
    return string(b)
}

のように独自のエラーの型が定義されていました。

このようにerrors.As()はエラーを型によって、比較したい場合に使われるということがわかりました。

終わりに

ドキュメントの説明をもう一度読み返すと、結局は実装を見てわかったことと同じようなことを言っているなぁと思いました。
ただ、実際のコードを読んでみることで、ドキュメントの理解度がより深まると思いました!

なので、これからも理解が曖昧なときにはどんな実装になっているのかな〜とソースコードを調べてみたいです!

もしこの記事の内容についてご指摘等ございましたら、お気軽に教えていただけると嬉しいです!!

そして、ライクルをはじめとして、SO Technologies では一緒に開発を行っていただけるエンジニアを大募集しております!
Go が好きな方!!!!Go に興味がある方!!!!ぜひ!!!
Goについて色々教えていただけると、とっても嬉しいです!!!!

こちらからぜひお気軽に!!

ここまで読んでいただいて本当にありがとうございます。