Go カスタムエラー ~xerros.Frame, errors~

こんにちは、マットです。
都内ITベンチャーのエンジニアです。
Go/Next.js/GraphQLを使っています。

今回はGo言語のエラー処理におけるカスタムエラーの実装を行なっていきます。 GraphQLのサーバーから構築している為、実装をGraphQL仕様に寄せています。

それでは独自のエラー構造体を定義してカスタムエラーを作成していきます。


Go 言語でカスタムエラー

まずは interfaceとstructをそれぞれ用意します。

type AppError interface {
    error
    Code() errorcode.ErrorCode
    SetCode(code errorcode.ErrorCode) AppError
    Info(infoMessage string) AppError
    InfoMessage() string
}

type appError struct {
    err         error
    message     string
    frame       xerrors.Frame
    errCode     errorcode.ErrorCode
    infoMessage string
}

AppError interfaceはアプリケーションでカスタムエラーを呼び出す為のinterfaceです。

appError structがGoのerror interfaceを実装するカスタムエラー構造体です。 Go 組み込みの error は以下のようなinterfaceとなっています。

type error interface {
    Error() string
}

Error() メソッドを実装すれば error として扱われます。

さらにAppErrorのメソッドを実装することで AppErrorとして使い回すことができます。

Wrap

Wrap()関数を定義します。 errorを返すコードは全てこのWrap関数でラップしてカスタムエラーのinterfaceであるAppErrorを返すようにします。

外部パッケージのカスタムエラー(gormなど)もこのWrap関数を使えば自前のカスタムエラーとして扱うことができます。

func Wrap(err error, msg ...string) AppError {
    var m string
    if len(msg) != 0 {
        m = msg[0]
    }
    e := create(m)
    e.err = err
    return e
}

New

errors.New() のようにエラーを生成したいときは独自のNew関数を作成します。 New関数の中では create関数をを呼び出し *appErrorを返します。

このときframe に スタックトレースを付与しています。


func create(msg string) *appError {
    var e appError
    e.message = msg
    e.frame = xerrors.Caller(2)
    return &e
}

func New(msg string) AppError {
    return create(msg)
}

HandleError

エラーハンドリングをまとめた関数を定義します。

func HandleError(ctx context.Context, err apperror.AppError) {

    var msg string

    msg += fmt.Sprintf("%+v", err)

    switch err.Code() {
    case errorcode.Validation:
        var validationErr *validationutil.ValidationErr
        if errors.As(err, &validationErr) {
            graphqlerr.AddValidationErr(ctx, validationErr)
        }
        log.Println(msg)
    case errorcode.NotFound:
        graphqlerr.AddErr(ctx, getInfoMessage(err), graphqlerr.NOT_FOUND_ERR)
        log.Println(msg)
    }
}


func getInfoMessage(apperr apperror.AppError) string {
    if apperr.InfoMessage() != "" {
        return apperr.InfoMessage()
    }

    if msg, ok := ErrMessageMap[apperr.Code()]; ok {
        return msg
    }

    return "internal server error"
}

今回はお試しでValidation, NotFoundのパターンを実装しました。 graphqlerrパッケージではgraphqlの仕様に沿ってエラーを吐き出しています。 ここはGraphQLの仕様に基づく実装なので割愛します。

getInfoMessage()では カスタムエラーにInfoMessageを付与していた場合はその文字列を返し、何も入っていない場合はSetCode()で付与したエラーメッセージを返します。

エラーを吐き出してみる

ここまででカスタムエラーの実装が完了したので試しにエラーを吐き出してみましょう。

func (r *queryResolver) Task(ctx context.Context, id string) (*gmodel.Task, error) {
    var task gmodel.Task

    if err := r.db.First(&task, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {

            gql.HandleError(
                ctx,
                apperror.Wrap(err, fmt.Sprintf("failed to get task id = %s", id)).SetCode(errorcode.NotFound),
            )
        } else {

            gql.HandleError(
                ctx,
                apperror.Wrap(err, fmt.Sprintf("failed to get task id = %s", id)).SetCode(errorcode.Database),
            )
        }
        return nil, nil
    }

    return &task, nil
}

上記はGraphQLのResolver内でDBから検索し、 見つからなければ NotFoundのカスタムエラーを返すようにしています。

出力結果

[10:58:27][APP] : 2021/12/26 10:58:27 failed to get task id = 13: record not found:
[10:58:27][APP] :     github.com/repo/backend/src/graphql/resolver.(*queryResolver).Task
[10:58:27][APP] :         /app/src/graphql/resolver/task.resolver.go:253
[10:58:27][APP] :   - record not found

無事にスタックトレース, エラーメッセージを出力することができました。

ここではWrapで gormパッケージのカスタムエラーをラップしています。

新しくエラーを生成したいときは次のようにします。

           gql.HandleError(
                ctx,
                apperror.New("new error").Info("this is Info").SetCode(errorcode.NotFound),
            )

Info()メソッドはカスタムエラーのにinfoMessageを付与するメソッドです。 infoMessageがある場合APIのエラーメッセージに表示させるような設計になっています。

以下 Info(), SetCode()

func (err *appError) Info(infoMessage string) AppError {
    err.infoMessage = infoMessage
    return err
}

func (err *appError) SetCode(code errorcode.ErrorCode) AppError {
    err.errCode = code
    return err
}

以上です。 実はこの記事を書こうと思った発端はロギングについて調べようと思ったときに前提としてカスタムエラーを理解しておく必要があると思ったからです。

という事で次回はロギングについてまとめたいと思っています。

最後に、 このブログではweb開発について発信していくのでまたご覧頂けると嬉しいです。 最後までお読み頂きありがとうございました。