Go Errorの基本的なこと

Posted on
go error

カスタムエラーについて理解が浅かったので改めて整理したいと思います。

基本形

仕様を確認

errorはinterfaceのため Error() string を満たせばいいとあります。

type error interface {
	Error() string
}

errosがまさにその実装を踏まえて作られております。

package errors

// errors.New()はその構造体を返す
func New(text string) error {
	return &errorString{text}
}

// errorStringというError() stringを満たす構造体を作る
type errorString struct {
	s string
}
func (e *errorString) Error() string {
	return e.s
}

実装してみる

同じように作ってみました。 エラー情報として欲しい内容を構造体に定義したら良さそうですね。

type TestError struct {
	first, second string
}

func (e TestError) Error() string {
	return fmt.Sprintf("%s --- %s", e.first, e.second)
}

func NewErr(first, second string) error {
	return &TestError{first, second}
}

func main() {
	fmt.Println(last())
}

func last() error {
	return NewErr("no.1", "no.2")
}


/*
$ go run main.go 
no.1 --- no.2
*/

Wrapの仕組みを調べる

とりあえず試してみます。

func main() {
	errA := errors.New("A error")
	errB := fmt.Errorf("B error, %w", errA)
	errC := fmt.Errorf("C error, %s", errA)
	errD := fmt.Errorf("D error, %w", errB)
	errE := fmt.Errorf("E error, %w", errC)

	fmt.Printf("errB type: %T // string: %s // unwrap: %s\n", errB, errB, errors.Unwrap(errB))
	fmt.Printf("errC type: %T // string: %s // unwrap: %s\n", errC, errC, errors.Unwrap(errC))
	fmt.Printf("errD type: %T // string: %s // unwrap: %s\n", errD, errD, errors.Unwrap(errD))
	fmt.Printf("errE type: %T // string: %s // unwrap: %s\n", errE, errE, errors.Unwrap(errE))
}


/*
errB type: *fmt.wrapError // string: B error, A error // unwrap: A error
errC type: *errors.errorString // string: C error, A error // unwrap: %!s(<nil>)
errD type: *fmt.wrapError // string: D error, B error, A error // unwrap: B error, A error
errE type: *fmt.wrapError // string: E error, C error, A error // unwrap: C error, A error
*/

fmt.Errorf(%w) を利用することで同じようにエラーをスタックしているように見えますが、errors.Unwrap() によって底にあるErrを取得できます

仕組みを見ると、Unwrap() ではまずは、Unwrap()を持っているかアサーションをかけ、入っていたらu.Unwrap() で取り出しています

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

上記の出力結果を見ると*errors.errorString には Unwrap() が最初に確認したように存在しなかったので nil になるようですね

*fmt.wrapError をみてみます

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
}

これを見ると、 wrapError{s, p.wrappedErr} にて、wrapError.msg に与えたerrを仕込み、Wrap対象を wrapError.err に入れていることがわかります

Unwrapの存在を確認

errors.Is()は同じエラーかを確認できる

次の場合は errA と同じインスタンスがWrapしたerrorに含まれているか確認できます。ただし同じインスタンスであることが条件なのでGlobal変数などで定義したものでないと使い回すのが難しそうです。

func main() {
	errA := errors.New("A error")
	errB := fmt.Errorf("B error, %w", errA)
	errC := fmt.Errorf("C error, %s", errA)
	errD := fmt.Errorf("D error, %w", errB)
	errE := fmt.Errorf("E error, %w", errC)

	fmt.Println(errors.Is(errB, errA)) // true
	fmt.Println(errors.Is(errC, errA)) // false
	fmt.Println(errors.Is(errD, errA)) // true
	fmt.Println(errors.Is(errD, errB)) // true
	fmt.Println(errors.Is(errE, errA)) // false
}

erros.As()は同じエラーの型かを確認できる

次のような場合はUnwrapした関数の中に MyError が入っているかをみています

type MyError struct {
	t time.Time
}

func (e MyError) Error() string {
	return fmt.Sprintf("This is MyError: %t", e.t)
}

func main() {
	errA := MyError{time.Now()}
	errB := fmt.Errorf("B error, %w", errA)
	errC := fmt.Errorf("C error, %s", errA)
	errD := fmt.Errorf("D error, %w", errB)
	errE := fmt.Errorf("E error, %w", errC)

	fmt.Println(errors.As(errB, &MyError{})) // true
	fmt.Println(errors.Is(errC, &MyError{})) // false
	fmt.Println(errors.As(errD, &MyError{})) // true
	fmt.Println(errors.Is(errD, &MyError{})) // false
	fmt.Println(errors.Is(errE, &MyError{})) // false
}

まとめ

以上のことを踏まえると、次のような使い方かと理解しました。

  • 深い階層にあるメソッドではWrapでerrを返す
  • Handlerに渡すときにカスタムエラーに整理して渡す
  • IsやAsなどでエラーハンドリングする