Go1.13からerrorsに色々機能が入った(The Go Blog)。

最近は新しいGoのことをインプットできていなかったので、リハビリがてらsonatardさんのまとめた記事も読みつつ、中の実装を眺めてまとめてみる。

Unwrap interface Link to heading

追加された機能は errors.Aserrors.Is のようにいくつかあるが、一番のキモとなる追加機能は errors.Unwrap になる。

似たような機能はデファクトスタンダードとして使われているpkg/errorsの errors.Cause としてありましたが、公式で導入されたことは非常に大きい。

とりあえず公式ライブラリの中を見に行ってみる。

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

これだけだった。

やっていることを言葉にしても、「渡されたerrorがUnwrap interfaceを実装していればUnwrapの返り値を返して、そうでなければnilを返す」だけ。

次はWrapする側の errors.Errorf の中を見にいってみる。

package fmt

import "errors"

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
}

type wrapError struct {
	msg string
	err error
}

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

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

長くなるのでコメントは削っている。

fmt.Errorf が呼び出されてからの4行は渡されたformatと引数をもとに pp 構造体を生成している部分。 深堀りはしないが、Unwrapの説明でも若干関わるので、pp 構造体はformatと変数のスライスをパースして色々やっているということだけは覚えておく。

Unwrapに関係があるのは5行目以降から。 4行目までで生成した pp 構造体の wrappedErr を確認して、nilだった場合 errors.New を、nilじゃない場合は同じ階層に定義してある wrapError 構造体を使ってエラーを作成している。

本筋からは少しずれるが、このような実装のため fmt.Errorf を呼び出すと必ずエラーが作成される。 そのため、nilの可能性があるエラーをnilチェックせずに渡すと思わぬバグの原因になるので、fmt.Errorf を呼び出すときには必ずnilチェックをするようにすること。

ここで重要になるのは wrapError 構造体。

wrapError 構造体はerror interfaceとUnwrap interfaceを実装している。 Unwrap()の中では wrapError が持っているerrを返しているが、このerrppwrappedErrを確認して詰められている。 また、3行目のdoPrintfの中を追いかけていくと、wrappedErrfmt.Errorfの第2引数に渡されたエラーが入っている事がわかる。

上記のことから、fmt.Errorf経由でWrapされたエラーがerrors.Unwrapに渡されたときには、fmt.Errorfに渡されたエラーが返ることになり、文字通りfmt.ErrorfでWrapされたエラーをUnwrapできる事がわかる。

Unwrap interface実用例 Link to heading

原因を取り出す Link to heading

pkg/errorsCauseと全く同じ挙動をする実装になる。 pkg/errorsではCause interfaceが使われていましたが、それをUnwrap interfaceに置き換えただけ。

package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.New("cause")
	err = fmt.Errorf("first: %w", err)
	err = fmt.Errorf("second: %w", err)
	err = fmt.Errorf("third: %w", err)
	fmt.Println(Cause(err))

	// Output:
	// cause
}

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

ただ、同一性を調べるためだったら errors.Is があるので、あまり上の例は出番がないかもしれない。

Wrapの途中に独自のエラー型が入っていないか調べる Link to heading

fmt.Errorf でWrapされたエラーは errors.As でWrapされてきた中に指定した方があった場合それを取り出すことができる。

以下の例だと途中でエラーにMarkをして、GetMarkerでそのMarkerを取り出している。

package main

import (
	"errors"
	"fmt"
)

type markErr struct {
	err    error
	marker string
}

func (e *markErr) Error() string {
	return fmt.Sprintf("%s: %v", e.marker, e.err)
}

func AddMark(err error, marker string) error {
	return &markErr{err, marker}
}

func GetMarker(err error) string {
	m := &markErr{}
	if ok := errors.As(err, &m); !ok {
		return ""
	}
	return m.marker
}

func main() {
	err := errors.New("cause")
	err = fmt.Errorf("first: %w", err)
	err = fmt.Errorf("second: %w", AddMark(err, "marker"))
	err = fmt.Errorf("third: %w", err)
	fmt.Println(GetMarker(err))

	// Output:
	// marker
}

しかし、Asの実装はUnwrap interfaceの実装が前提のため、不用意に独自エラーをWrapに挟むと深い階層でWrapしたエラーを取り出せなくなってしまう。

以下の例だと、GetMarkerの途中でエラーをMarkしたMarkerを取り出したいが、Markした後にcodeErrでWrapしているためMarkerを取り出せていない。

package main

import (
	"errors"
	"fmt"
)

type markErr struct {
	err    error
	marker string
}

func (e *markErr) Error() string {
	return fmt.Sprintf("%s: %v", e.marker, e.err)
}

func AddMark(err error, marker string) error {
	return &markErr{err, marker}
}

type codeErr struct {
	err  error
	code int
}

func (e *codeErr) Error() string {
	return fmt.Sprintf("%d: %v", e.code, e.err)
}

func AddCode(err error, code int) error {
	return &codeErr{err, code}
}

func GetMarker(err error) string {
	m := &markErr{}
	if ok := errors.As(err, &m); !ok {
		return ""
	}
	return m.marker
}

func GetCode(err error) int {
	c := &codeErr{}
	if ok := errors.As(err, &c); !ok {
		return 0
	}
	return c.code
}

func main() {
	err := errors.New("cause")
	err = fmt.Errorf("first: %w", err)
	err = fmt.Errorf("second: %w", AddMark(err, "marker"))
	err = fmt.Errorf("third: %w", AddCode(err, 3))
	fmt.Println(GetMarker(err))
	fmt.Println(GetCode(err))

	// Output:
	//
	// 3
}

errors.Asの実装はUnwrap interfaceを満たしていることが前提のため、独自のエラー型を定義するときには同時にUnwrap interfaceを満たすように実装すると、期待する動作をしてくれるようになる。

御存知の通りGoはDuck TypingなのでUnwrap interfaceを満たすには型が Unwrap() を実装するだけでよい。

package main

import (
	"errors"
	"fmt"
)

type markErr struct {
	err    error
	marker string
}

func (e *markErr) Error() string {
	return fmt.Sprintf("%s: %v", e.marker, e.err)
}

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

func AddMark(err error, marker string) error {
	return &markErr{err, marker}
}

type codeErr struct {
	err  error
	code int
}

func (e *codeErr) Error() string {
	return fmt.Sprintf("%d: %v", e.code, e.err)
}

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

func AddCode(err error, code int) error {
	return &codeErr{err, code}
}

func GetMarker(err error) string {
	m := &markErr{}
	if ok := errors.As(err, &m); !ok {
		return ""
	}
	return m.marker
}

func GetCode(err error) int {
	c := &codeErr{}
	if ok := errors.As(err, &c); !ok {
		return 0
	}
	return c.code
}

func main() {
	err := errors.New("cause")
	err = fmt.Errorf("first: %w", err)
	err = fmt.Errorf("second: %w", AddMark(err, "marker"))
	err = fmt.Errorf("third: %w", AddCode(err, 3))
	fmt.Println(GetMarker(err))
	fmt.Println(GetCode(err))

	// Output:
	// marker
	// 3
}

新しくerrorsパッケージに入った機能はUnwrapという非常に薄いinterfaceで提供されていながら、とても応用の効く機能だった。