🐁

Go のエラーとエラーハンドリングと

2023/12/16に公開

はじめに

https://qiita.com/advent-calendar/2023/go

※ 12.16 に投稿していますがシリーズ 2 の 8 日目が空いていたので追加しました。

Go のエラーとエラーハンドリングについては数多と記事が書かれていると思いますが自分なりの言葉でまとめてみたい気持ちになったので書いています。
Go のエラーは愚直にチェックをする必要がありますが、私はそういうところが好きだったりします。

Go のエラー

Go は公式サイト Why does Go not have exceptions? で言及されているように try-catch のような例外処理がありません。
下記で示すように関数の戻り値として error が返ってきます。それを都度都度 nil チェックを実施し処理します。

if err := Func(); err != nil {
	// Do something
}

公式ブログ Error handling and Go に記載がある通り、 Go は以下のような interface で定義されています。

type error interface {
	Error() string
}

Go の interface は定義されているメソッドが構造体で実装されていれば、その interface を満たしていると判定されます。( = Duck Typing)
つまり、以下のように独自の error を定義することもできます。

type UniqueError struct {
	Msg string
}

func (ue *UniqueError) Error() string {
	return ue.Msg
}

Go でエラーを返す

Go でエラーを返すパターンはいくつかあります。

errors.New() を返す

https://pkg.go.dev/errors#New

ex)

func Func() {
	return errors.New("error")
}
定義
func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

引数で与えた文字列が Error() メソッドで取得できるエラーが生成できます。

fmt.Errorf() を返す

https://pkg.go.dev/fmt#Errorf

ex)

func Func() error {
	return fmt.Errorf("error")
}
if err := Func(); err != nil {
	return fmt.Errorf("failed to Func: %w", err)
}
定義
func Errorf(format string, a ...any) error {
	p := newPrinter()
	p.wrapErrs = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	switch len(p.wrappedErrs) {
	case 0:
		err = errors.New(s)
	case 1:
		w := &wrapError{msg: s}
		w.err, _ = a[p.wrappedErrs[0]].(error)
		err = w
	default:
		if p.reordered {
			sort.Ints(p.wrappedErrs)
		}
		var errs []error
		for i, argNum := range p.wrappedErrs {
			if i > 0 && p.wrappedErrs[i-1] == argNum {
				continue
			}
			if e, ok := a[argNum].(error); ok {
				errs = append(errs, e)
			}
		}
		err = &wrapErrors{s, errs}
	}
	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
}

type wrapErrors struct {
	msg  string
	errs []error
}

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

func (e *wrapErrors) Unwrap() []error {
	return e.errs
}

fmt.Sprintf() と同じような形式でエラーを生成できます。
%w を 引数format に含め a として error を渡すことでエラーを Wrap することができます。
また、 実装を眺めていると error は複数渡せるとわかります。
(恥ずかしながら実装を眺めて初めて知りました。)

※ Wrap と Unwap については後述します。

そのまま返す

ここでは何もせずそのまま error を返します。
ログを仕込むなどしてもよいでしょう。

ex)

if err := Func(); err != nil {
	slog.Warn(err.Error())

	return err
}

独自エラー を返す

独自に定義したエラーを返します。

ex)

if err := Func(); err != nil {
	return &UniqueError{Msg: err.Error()}
}

なぜエラーってハンドリングする??

エラーをハンドリングする理由はエラーが発生した場合に応じて処理を実行したいからだと思います。
雑に実装するなら下記のようになるでしょうか。

if err := Func(); err != nil {
	panic(err)
}

プログラムが続行できないと判断した場合に panic を実行しランタイムを停止させます。
公式サイト panic にて言及されていますが、大抵の場合はプログラムを実行し続けるように振る舞うように処理するでしょう。

例えばエラーをハンドリングしたあとの処理は以下のようなものが挙げられるでしょう。

  • 特定のエラーを無視したい
  • 特定のエラーをHTTPステータスコードに変換したい

このように"特定のエラー"に対して処理を行います。
Go において"特定のエラー"を判定するためには errors.Is()errors.As() を利用する必要があります。

errors.Is(err, target error)

https://pkg.go.dev/errors#Is

Is はエラーのインスタンスが同一かどうかをチェックする関数です。
以下のような実装例で活用します。

ex) サーバー起動時のエラーチェック

if err := http.ListenAndServe(":8080", nil); err != nil && errors.Is(err, http.ErrServerClosed) {
	panic(err)
}
http.ErrServerClosed
var ErrServerClosed = errors.New("http: Server closed")

簡潔に述べると var で宣言されたエラーかどうか比較していると言えるでしょう。

errors.As(err, target error)

https://pkg.go.dev/errors#As

As はエラーの型に代入可能かどうかをチェックする関数です。
以下のような実装例で活用します。

ex) PostgreSQL のエラーチェック

func Handle(w http.ResponseWriter, r *http.Request) {
	client, _ := sql.Open("postgres", "dsn")

	if _, err := client.ExecContext(context.Background(), "SQL"); err != nil {
		w.WriteHeader(ToHTTPStatus(err))

		return
	}

	w.WriteHeader(http.StatusOK)
}

func ToHTTPStatus(err error) int {
	var target *pq.Error

	if errors.As(err, &target) {
		switch target.SQLState() {
		case "23505":
			return http.StatusConflict
		// ...
		default:
			return http.StatusInternalServerError
		}
	}

	return http.StatusInternalServerError
}
pq.Error
type Error struct {
	Severity         string
	Code             ErrorCode
	Message          string
	Detail           string
	Hint             string
	Position         string
	InternalPosition string
	InternalQuery    string
	Where            string
	Schema           string
	Table            string
	Column           string
	DataTypeName     string
	Constraint       string
	File             string
	Line             string
	Routine          string
}

type ErrorCode string

// ...

func (err *Error) Error() string {
	return "pq: " + err.Message
}

簡潔に述べると独自定義したエラーかどうか比較していると言えるでしょう。

Is と As の実装

errors.Is()
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
		}
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
		case interface{ Unwrap() []error }:
			for _, err := range x.Unwrap() {
				if Is(err, target) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}
errors.As()
func As(err error, target any) bool {
	if err == nil {
		return false
	}
	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")
	}
	targetType := typ.Elem()
	if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	for {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
			return true
		}
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
		case interface{ Unwrap() []error }:
			for _, err := range x.Unwrap() {
				if As(err, target) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}

どちらの実装にも共通しているのが Unwrap() メソッドをチェック/実行しているということです。

switch x := err.(type) {
case interface{ Unwrap() error }:
	err = x.Unwrap()
	if err == nil {
		return false
	}
case interface{ Unwrap() []error }:
	for _, err := range x.Unwrap() {
		if As(err, target) {
			return true
		}
	}
	return false
default:
	return false
}

Unwrap() メソッド

errorIsAs で検証するとき Unwrap() が呼び出されるということがわかりました。
つまり、 error が Wrap されているかどうかが重要となります。

※ Wrap とはエラーを新たに作成する際にオリジナルのエラーを保有している状態( = Unwrap() で返せる状態)と言えるでしょう。

errors.New() 関数はその場で error を作成しているので Wrap は関係ないです。Unwrap() することを前提としていない関数と言えるでしょう。( = Wrap をしたいのであれば適していない関数です。)
fmt.Errorf() 関数は %w, error で渡した error を内部に保有していており Unwrap() でその error が返されるように実装されています。

ここで問題になってくるのが独自エラーです。

Go における errorfunc Error() string メソッドが構造体に実装されていればエラーとなります。その独自エラーが errors.New() と同じような使い方であれば Wrap する必要はありません。
しかし、独自エラーを構築する多くの場合はなにかしらの error を保有するように定義するかと思います。
そのときは、ぜひ Unwrap() メソッドも忘れずに実装してください。

type UniqueError struct {
	Msg string
	Err error
}

func (ue *UniqueError) Error() string {
	return ue.Msg + ":" + ue.Err.Error()
}

func (ue *UniqueError) Unwrap() error {
	return ue.Err
}

もしかしたらエラーハンドリングする側がオリジナルのエラーをチェックしているかもしれません。
エラーを保有していてもUnwrap() が実装されていなければ、それはエラーを握りつぶしているも同然です。

おわりに

自分の言葉で Go のエラーについていろいろと書いてみました。
伝えたかったことは独自エラーを作るなら Unwrap() メソッドも忘れずにということです。
誰かのお役に立てれば幸いです。
間違いなどあればご指摘いただけると幸いです。

Discussion