Go カスタムエラー ~xerros.Frame, errors~
今回は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開発について発信していくのでまたご覧頂けると嬉しいです。 最後までお読み頂きありがとうございました。