11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

go1.13のerrorのラップ

Posted at

#Go1.13からエラーがラップできるようになった
Go1.13のバージョンからエラーのラップができるようになったみたいなので、自分なりにまとめてみました。

##新しく追加された関数

  • func As(err error, target interface{}) bool
  • func Is(err, target error) bool
  • func New(text string) error
  • func Unwrap(err error) error

エラーのラップ方法

まず、エラーのラップ方法はこんな感じになります

wrapError.go
package main

import(
	"errors"
	"fmt"
)

var MyError = myError()

func myError() error { return errors.New("myError")}

func makeMyError() error {
	return MyError
}

func makeWrappedError() error {
	err := makeMyError()
	return fmt.Errorf("%w", err)
}

func main() {
	err := makeMyError()
	fmt.Printf("%T\n", err)

	wrappedErr := makeWrappedError()
	fmt.Printf("%T\n", wrappedErr)
}

このようにエラーをwrapするときには fmt.Errorf("%w", err) を使います。
実行結果は以下

*errors.errorString
*fmt.wrapError

きちんとwrapされているのがわかります。
今度はこのエラーを取り出すのにUnwrapします。

unwrap.go

func main() {
	wrappedErr := makeWrappedError()
	fmt.Printf("type:%T", wrappedErr)

	unWrappedErr := errors.Unwrap(wrappedErr)
	fmt.Printf("type:%T", unWrappedErr)

	nilErr := errors.Unwrap(unWrappedErr)
	fmt.Printf("type:%T", nilErr)
}

結果は以下。

type:*fmt.wrapError
type:*errors.errorString
type:<nil>

このようにUnwrapされて行き、wrapしていないエラーをUnwrapすると、nilを返すようになります。
ここまでがエラーのラップについてです。

##Is関数の使い方
エラーのラップはわかっていただけたと思いますが、これをどうやって使うの?という話になります。
まず、今までのエラーハンドリングについておさらいします。こんな感じ

errorHandle.go
func main() {
	err := makeMyError()
	if err != nil {
		switch err {
		case MyError:
			fmt.Printf("MyError: %s", err.Error())
		default:
			fmt.Printf("defaultError: %s", err.Error())
		}
	}
}

結果は以下

MyError: myError

これで、MyErrorを拾えています。ここでエラーをラップしたものに変えてみます。

wrapErrorHandle.go
func main() {
	err := makeWrappedMyError()
	if err != nil {
		switch err {
		case MyError:
			fmt.Printf("MyError: %s", err.Error())
		default:
			fmt.Printf("defaultError: %s", err.Error())
		}
	}
}

結果は

defaultError: myError

となってしまし、MyErrorとして拾えていません。ここで、Is関数の登場です。
Is関数を使うとこのようになります。

Is.go
func main() {
	err := makeWrappedError()
	if err != nil {
		if errors.Is(err, MyError) {
			fmt.Printf("MyError: %s", err.Error())
		} else {
			fmt.Printf("defaultError: %s", err.Error())
		}
	}
}
結果
MyError: myError

今度は、きちんとMyErrorとして拾えました。
このように、今までのエラーハンドリングでは扱えないパターンが出てくるので、Is関数を用いる必要が出てきます。

##As関数の使い方
As関数もIs関数と使う目的は同じですが、引数を見ればわかるように自分で定義したエラーとの比較などを行うときに用います。
このような独自のエラーがあるとしましょう。

myError.go
package main

import(
	"fmt"
)

type marshalError struct {
	msg string
	code int
}

func (e *marshalError)Error() string {
	return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
	return &marshalError{msg:"hoge", code:0}
}

func main() {
	err := marshal()
	if e, ok := err.(*marshalError); ok {
		fmt.Printf("%s", e.Error())
	} else {
		fmt.Printf("defaultError")
	}
}

結果
marsshal error :hoge, code: 0

これで、構造体をマーシャル(仮想で)して帰ってきたエラーがエラーがmainErrorであることを確かめられました。
しかし、このエラーがまたラップされていた場合拾えなくなってしまいます。

wrappedMyError.go
package main

import(
	"fmt"
)

type marshalError struct {
	msg string
	code int
}

func (e *marshalError)Error() string {
	return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
	return &marshalError{msg:"hoge", code:0}
}

func wrapError() error {
	err := marshal()
	return fmt.Errorf("%w", err)
}

func main() {
	err := wrapError()
	if e, ok := err.(*marshalError); ok {
		fmt.Printf("%s", e.Error())
	} else {
		fmt.Printf("defaultError")
	}
}
結果
defaultError

これを避けるためにAs関数を使います。

As.go
package main

import(
	"errors"
	"fmt"
)

type marshalError struct {
	msg string
	code int
}

func (e *marshalError)Error() string {
	return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
	return &marshalError{msg:"hoge", code:0}
}

func wrapError() error {
	err := marshal()
	return fmt.Errorf("%w", err)
}

func main() {
	err := wrapError()
	var targetErr *marshalError
	if errors.As(err, &targetErr) {
		fmt.Printf("%s", targetErr)
	} else {
		fmt.Printf("defaultError")
	}
}


結果
mainError: main error

これで、mainErrorを拾えました。

最後にこれを応用して、エラーの抽象度をあげて、上位の関数に返すようなコードを考えてみました。

Example.go
package main

import(
	"errors"
	"fmt"
)

type User struct {
	name string
	email string
}
type invalidStructError struct {
	original interface{} //元の構造体
	err error
	code int
}

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

func marshal(v interface{}) error {
	err := &invalidStructError{original:v, err: errors.New("invalid Struct"), code: 0}
	if err != nil{
		return fmt.Errorf("marshal err: %w", err)
	}
	return nil
}

func main() {
	val := User{name: "hoge", email: "1"}
	err := marshal(val)
	var target *invalidStructError
	if errors.As(err, &target) {
		fmt.Printf("code: %d\n", target.code)
		fmt.Printf("%s\n", target.Error())
	} else {
		fmt.Printf("defaultError")
	}
}
結果
code: 0
original: {hoge 1}, invalid Struct

As関数を用いてtargetにerrをUnwrapしているので、codeのフィールドが扱えるようになりました。

##まとめ
このように、Go1.13ではエラーのラップができるようになりました。これにより、エラーをより上位の関数に渡すときに抽象化して渡すことができるようになります。
それに伴って、エラーのハンドリングが変わってくるので、新しい形式をうまく使いこなせると良さそうです。
まだまだgo初心者なので、間違いなどありましたらご指摘いただけると幸いです。

###参考サイト
https://cipepser.hatenablog.com/entry/go1.13-error-wrapping
https://golang.org/pkg/errors/#pkg-index

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?