errorsパッケージに興味持った

v1.13からerrorsパッケージに Unwrap() Is() As() といった関数が追加されました。
(もう1.14もリリースされているのに今さらですね😇)

今回はこれら3つの関数について、内部実装を追いかけていきます。

と、その前に、errorsパッケージの概要と関連パッケージについて軽く説明しておきます。

errorsパッケージと関連パッケージ

errorsパッケージ

名前の通り、エラー関連の処理がまとまっているパッケージですね。
Goの標準パッケージです。
→ GoDoc

v1.13にて、先述の Unwrap() Is() As() という関数たちが追加されました。

errorを扱うパッケージとして、もうひとつ有名なパッケージがあります。
xerrorsパッケージです。

xerrorsパッケージ

xerrors とは、 Goのサブリポジトリ で開発が進められているパッケージです。
(準標準パッケージといった感じでしょうか)

xerrorsのGoDoc に下記の記述がある通り、

These functions were incorporated into the standard library’s errors package in Go 1.13: - Is - As - Unwrap

もともとは本パッケージに Unwrap() Is() As() が実装されていましたが、
v1.13にて標準パッケージに取り込まれました。


さて、軽くerror関連のパッケージについて触れたところで、
早速、Unwrap() Is() As() の内部実装を見ていきたいと思います。
なお、Goのコードはv1.14.0を参照しています。

Unwrap()

ラップされたエラーから中身のエラーを取り出す関数です。

処理としては下記のようになっています。

func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

https://golang.org/src/errors/wrap.go?s=372:400#L14

ぱっと見だと、ん?っとなってしまうかもしれませんが、
下記のように処理を分解してやると、特別難しいことは何もしていないことがわかります。

func Unwrap(err error) error {
    // ラップされたエラーのインターフェース
    type wrapErrInterface interface {
        Unwrap() error
    }

    // 型アサーションにより、ラップされたエラーのインターフェースを満たしているかチェック
	u, ok := err.(wrapErrInterface)
	if !ok {
		return nil
	}
	return u.Unwrap()
}

処理を順に追っていくと、 7行目で型アサーションを用いてラップされたエラーのインターフェースを満たしているかチェックし、 満たしていなければ(ok == false)nilを返します。
満たしていれば(ok == true)実装されている Unwrap() を処理します。

ここで注意ですが、 12行目の Unwrap() は今まで話に出てきていた errors.Unwrap() とは全くの別物です。
では、12行目の Unwrap() はどこにあるのか。
答えはerrorをラップする処理のところにあります。

errorをラップする関数

errorをラップする関数である fmt.Errorf() の中身を見てみましょう。

func Errorf(format string, a ...interface{}) error {
	p := newPrinter()
	p.wrapErrs = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	if p.wrappedErr == nil {
		err = errors.New(s)
	} else {
		err = &wrapError{s, p.wrappedErr}
	}
	p.free()
	return err
}

https://golang.org/src/fmt/errors.go?s=624:674#L17

10行目で、wrapError という構造体を返していますね。
宣言箇所に飛んでみましょう。

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

https://golang.org/src/fmt/errors.go#L32

Unwrap() がありました。

まず、wrapError構造体ですが、本構造体はerrフィールドを持っており、ここにラップするエラーを格納しています。
(さきほど見た Errorf() の内部処理では、10行目にてwrapErrorが使用されています)

Unwrap()はwrapError構造体のerrフィールド、すなわち、ラップしていたエラーを返しているだけですね。


以上、errors.Unwrap() の内部実装はこんな感じでした。

どんどん行きましょう。

Is()

次は Is() を見ていきます。

本関数は2つのエラーが同じエラーかどうかを判定します。
また、比較元(第一引数)のエラーがラップしたエラーだったとしても、
最後までUnwrapして比較してくれます。

処理はこんな感じです。

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

	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}

要となる処理は7〜20行明のfor文内の処理です。

まずは、8,9行目にて単純にエラー同士の比較をしています。
ここで一致すれば return true ですね。

次に11行目で、型アサーションを利用して errが Is(error) bool という関数を実装しているかチェックしています。

このチェック処理は、 独自の同値判定処理がないか確認し、ある場合はその同値判定処理を使用して判定を行う
ために用意されています。

Is(error) bool の実装例が公式のドキュメント にあります。

↓↓↓

func (m MyError) Is(target error) bool { return target == os.ErrExist }

独自のエラー型を定義するときに役立ちそうですね。

では、最後に15行目からの処理です。
ここはerrをUnwrapする処理ですね。
(このUnwrap()は前章で説明した関数です)

つまり、
isComparable && err == target
および
x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target)
の両条件に該当しなかった場合は、errの中にあるエラーを抜き取り、
そのエラーに対して、forループの最初から処理していくということになります。

この最後のUnwrap()により、本章冒頭に述べた

また、比較元(第一引数)のエラーがラップしたエラーだったとしても、
最後までUnwrapして比較してくれます。

というのを実現しているわけですね。

As()

最後に As() です。

本関数は、第一引数のエラーが第二引数のエラーに代入可能であれば代入し、trueを返します。
代入できない場合はfalseが返されます。

第二引数はポインタ型なので、targetに関して副作用を含む関数です。

それでは内部実装を見ていきます。

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

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

for文と errors.Unwrap() を使って
ラップされたエラーの中身を取り出していくあたりは Is() と同じですね。
加えて、19行目で独自定義の As() を使用できるところも Is() と同じです。

特徴的なのは、5〜18行目の部分になります。

まず、5,6行目でreflectliteを使って第二引数のtargetの構造を読み取っています。

reflectliteはreflectパッケージの軽量版で、
runtimeおよびunsafe以外での使用は基本的に禁止されています。 » 参考

そして、targetがポインタである、かつ、nilでないことを確認します。

本章冒頭でも述べましたが、
最終的に(代入可能であれば)第一引数のerrは第二引数のtargetに格納します。
つまり、戻り値でtargetに格納したエラーを返すのではなく、target(ポインタ)経由でできあがったエラーを返します。
したがって、ポインタであることを確認する必要があります。

加えて、10行目で、interfaceである、かつ、errorType(=error)を実装できているかチェックします。

以上で、targetがerrorを格納できる箱であるか(errorインタフェースを満たしているか)どうかを判定しています。


続きの13行目以降で、
errがtargetに格納できる値かどうかを判定し、できるならば格納しています。(15,16行目)

格納できない場合は、独自実装の As() 探して、実行していますね。

err が target に格納できず、独自実装の As() もない場合は、
err を Unwrap() して再度同じ処理を行います。

それでも、格納できるエラーがなかった場合は false を返します。

まとめ

errorsパッケージの実装を覗いてみましたが、いかがだったでしょうか?
普段使ってる標準パッケージの内部実装を追いかけるのは楽しいですね👍

今回はerrorsパッケージの中身を見ましたが、reflectliteパッケージが結構使われていましたね。

reflectliteの動きが分からない部分もあったので、
次はreflectliteの中身も見たいなという気持ちになっています。

(reflectliteを少し覗いたのですが、Goの型のデータ構造?的な話が入ってきており、かなりおもしろそう)

reflectliteを一緒に読みたいって方おられたらTwitterでDM ください!
ぜひオンラインでコードリーディング会しましょう

参考文献