yyh-gl's icon

yyh-gl's Tech Blog

技術ネタ中心のブログです。主な扱いはバックエンド技術と設計です。

【Go】errorsパッケージの中身覗いてみた

今回は Unwrap(),Is(),As() についてお届け

yyh-gl

3 分で読めます

featured

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行目で、型アサーションを利用して errIs(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)を実装できているかチェックします。

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


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

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

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

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

まとめ

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

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

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

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

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

参考文献

最近の投稿

About

東京で働くソフトウェアエンジニアです。バックエンドがメインですが、フロントエンドやインフラもさわっています。