Go + クリーンアーキテクチャにおけるエラーハンドリング戦略

2024/07/07に公開

はじめに

こんにちは!mizukoです!
先日PaPutという個人開発のサービスをβ版としてリリースしました!

その際、今後の個人開発ライフのために基盤作りを行ったのですが、
エラーハンドリングの戦略も検討したので、アウトプットしていきたいと思います!

PaPutについてはぜひこちらをご覧いただけますと幸いです🤗

前提

  • プロジェクトはクリーンアーキテクチャの構成
    (本記事で紹介するエラーハンドリング方法はどの様な構成でも使えると思いますが、私自身がこの構成でしか運用したことが無いのため、クリーンアーキテクチャを前提とさせていただいています。)

戦略

独自のエラーハンドリングにならないよう、シンプルに以下戦略で進めます...!

  • errorの管理はプロジェクトルートにerrors/errors.goを作成し、一元管理する
  • 全てerror型で扱い、error型をラップして新しい構造体を作るなどは行わない
  • errorはerrors.Isで比較する

errorを一元管理する

クリーンアーキテクチャにおいて、errorをどこで管理するかは悩ましいところです。
domain層で各entityに関連するエラーを定義するという方針も取れそうですが、

  • 汎用的なerrorの置き場所に困る
  • adapter層にだけ関わるerrorもドメイン層に置くことになる

などの点から、プロジェクトルートのerrors/errors.goに管理することにしました。

実際のソース

実際のソースは以下です。

package errors

import (
	"errors"
	"fmt"
)

// 汎用エラー
var (
	ErrInvalidRequest        = errors.New("リクエストが無効です。")
	ErrInternalServerError   = errors.New("エラーが発生しました。")
	ErrInvalidRequestPayload = errors.New("リクエストの形式が不正です。")
)

// ユーザー関連エラー
var (
	ErrFailedCreateUser  = errors.New("ユーザーの作成に失敗しました。")
	ErrUserNotFound      = errors.New("ユーザーが見つかりませんでした。")
	ErrUserAlreadyExists = errors.New("既に存在するユーザーです。")
)

// 認証関連エラー
var (
	ErrUnAuthorized = errors.New("認証に失敗しました。")
)

func New(msg string) error {
	return errors.New(msg)
}

func Is(err, target error) bool {
	return errors.Is(err, target)
}

func NewNotFoundError(entity, id string) error {
	return fmt.Errorf("%s (ID: %s) が見つかりませんでした。", entity, id)
}

// 他にもあれば上記と同じように定義していく

トピックとしては以下です。

  • error型を定数(変数)として定義する
  • NewやIsなど、標準のerrorsパッケージをラップしている
    • Isなどを標準パッケージから使おうとすると、import時の名前が衝突してしまうため、使い勝手を考えてラップした
  • NotFoudの様な「何が」があるようなエラーは、fmtでerrorを返す

利用例

例えば、以下のようにusecase層でエラーを返した場合

user, err := i.UserRepository.FindByEmail(ctx, tokenData.Email)

// 既にユーザーが存在する場合はエラーを返す
if user != nil {
    return nil, errors.ErrUserAlreadyExists
}

if err != nil {
	return nil, err
}

adapter層で以下の様にチェックし、ハンドリングします。

if err != nil {
	if errors.Is(err, errors.ErrUserAlreadyExists) {
		helper.HandleNotFound(c, err)
	} else {
		helper.HandleInternalServerError(c, errors.ErrFailedCreateUser)
	}

	return
}

これにより、sqlパッケージのエラー比較(例えば、sql.ErrNoRows)なども同じ使用感でエラーハンドリングできるため一貫した開発が行えます...!

おわりに

Go + クリーンアーキテクチャで行うエラーハンドリング戦略について記載してみました!
より良い方法知っている有識者の方がいましたらぜひご指摘お願いします🙏
Goでアプリケーションでクリーンアーキテクチャを採用しており、エラーハンドリングについて迷っている人がいましたらぜひ参考にしてみてください!

冒頭でも記載しましたが、PaPutというサービスのβ版をリリースしました!
是非手軽に使って、フィードバックいただけますと嬉しいです!
PaPutについてはぜひこちらもご覧ください!

Discussion