15
8

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 5 years have passed since last update.

xerrors パッケージで途中に独自定義したエラー型をラップする方法

Last updated at Posted at 2019-02-17

GoのWebアプリケーションのエラー設計 の最後で紹介したように、エラーを変換する際に下位の情報をラップすることが xerrors.Errorf ではできません。

var ErrUserNotFound = NewApplicationError("Error", 101, "not found")

func NewApplicationError(level string, code int, msg string) *ApplicationError {
    err := &ApplicationError{
        level: level,
        code:  code,
        msg:   msg,
    }

    return err
}

func main() error {
    user, err := userSearch()
    if err != nil {
        // return ErrUserNotFound ではなく以下のようにしたいが、複数のエラーのラップには対応していない
        return xerrors.Errorf(": %w: %w", ErrUserNotFound, err)
    }

    fmt.Println(user)
}

xerrorsにはこれを解決するための方法が用意されているため紹介します。

独自のエラー型に xerrors.Formatter を実装する。

標準の errors パッケージに入るまでは fmt.Formatter も実装する必要があります。

type ApplicationError struct {
	level string
	code  int
	msg   string
        // ラップする
	err   error
        // frame情報を保持する
	frame xerrors.Frame
}

func NewApplicationError(level string, code int, msg string) *ApplicationError {
    return &ApplicationError{
        level: level,
        code:  code,
        msg:   msg,
        err:   nil,
        frame: xerrors.Caller(1),
    }
}

// Here is not pointer receiver.
func (e ApplicationError) Wrap(next error) error {
    // ラップするエラーを渡す
    e.err = next
    // frameにCallerを設定
    e.frame = xerrors.Caller(1)
    return &e
}

func (e *ApplicationError) Error() string {
	return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.code, e.msg)
}

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

// fmt.Formatterを実装
func (e *ApplicationError) Format(s fmt.State, v rune) { xerrors.FormatError(e, s, v) }

// xerrors.Formatterを実装
func (e *ApplicationError) FormatError(p xerrors.Printer) (next error) {
	p.Print(e.Error())
	e.frame.Format(p)
	return e.err
}

使い方

var ErrUserNotFound = NewApplicationError("Error", 101, "not found")

func main() {
    user, err := userSearch("12345")
    if err != nil {
	// return ErrUserNotFound
	return ErrUserNotFound.Wrap(err)
    }

    fmt.Println(user)
}

おおげさな実装ではありませんが、自分で定義したエラー型すべてに追加するのは多少手間になるのでライブラリを作ってみました。

sonatard/werrorは、定義したエラー型に埋め込むことで利用します。

Go 1.12までは werror "github.com/sonatard/werror/xerrors" 、 Go 1.13以降は "github.com/sonatard/werror" を import してご利用ください。

package main

import (
    // Before Go 1.13
    werror "github.com/sonatard/werror/xerrors"
    // From Go 1.13
    // "github.com/sonatard/werror"
)

type ApplicationError struct {
	level string
	code  int
	msg   string
	werror.WrapError
}

// Here is not pointer receiver.
func (e ApplicationError) Wrap(next error) error {
	e.WrapError = werror.Wrap(&e, next, 2)
	return &e
}

func (e *ApplicationError) Error() string {
	return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.code, e.msg)
}

また独自に定義した型を単純に比較してしまうとトレース情報が含まれた構造体を比較するため false になってしまうことがあります。
そのため必ず Is メソッドを定義することをオススメします。
xerrors パッケージ - 独自に定義したエラー型はIsメソッドとAsメソッドでデフォルトの振る舞いを変更可能

さいごに

Proposal: Go 2 Error Inspection には、以下のように書いてあり、フィードバックとして複数のエラーをラップすることを期待している人が多いみたいですが、現時点では実装の予定はないようです。

Lastly, we want to acknowledge the several comments on the feedback wiki that suggested that we go further by incorporating a way to represent multiple errors as a single error value. We understand that this is a popular request, but at this point we feel we have introduced enough new features for one proposal, and we’d like to see how these work out before adding more. We can always add a multi-error type in a later proposal, and meanwhile it remains easy to write your own.

関連情報

15
8
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
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?