LoginSignup
0
1

More than 1 year has passed since last update.

Go言語のerrorにエラー情報を付けて分岐する。

Last updated at Posted at 2022-03-01

この文章について

Go言語のerrorはシンプルでerrors.New("エラーメッセージ")で生成することができます。その一方で、エラー内容に応じて分岐するには工夫が必要です。本記事ではGo言語のerrorでエラー内容に応じて分岐する方法を記載します。

例えば、ユーザ情報を取得するREST APIを作ったときに、

  • ユーザが見つからないエラーの場合は、404レスポンス
  • データベース接続時に不測のエラーがあった場合は、500レスポンス

としたいと思います。JavaやC#等であれば例外クラスのどれをキャッチしたかで判断できます。しなしながら、Go言語ではどちらの場合であってもシンプルなerrorとだけ変えるので判断できません。そこで判断できるように工夫をしましょう。

方法1:Sentinel Error

「Sentinel Error」という単語の和訳が分かりませんので、ここでは「Sentinel Error」と書きます。(知っている方がいましたら教えてください。)

これはerrors.New(エラーメッセージ)をあらかじめ定義しておき、比較することで判断する物です。

以下に例を示します。
UserNotFoundErrorをあらかじめ定義し、このエラーを公開しておきます。
呼び出し元では取得したエラーがUserNotFoundErrorかどうかを比較して分岐しています。

// ユーザが見つからないエラーを定義
var UserNotFoundError = errors.New("user not found")

func getUser(userID: Int) (*model.User, error) {
    // ユーザが見つからないエラーを返す
    return nil, UserNotFoundError
}

func main() {
    user, err := getUser(3)
    if errors.Is(err, UserNotFoundError) {
        fmt.Println("ユーザが見つかりませんでした。")
        return
    }
    if err != nil {
        panic("意図しないエラーが発生しました。")
    }

    fmt.Printf("ユーザ情報: %#v", user)
}

これで立派に分岐ができました。エラーメッセージの"user not found"で比較しています。

実際、SQLで該当する行が見つからなかった場合のエラーはこれで分岐しているようです。
( 参考に https://go.dev/src/database/sql/sql.go#L442 をご覧下さい。)

※エラー型同士を比較するときは errors.Is()を使うようですね。

var ErrNoRows = errors.New("sql: no rows in result set")

この方法の問題点として次が上げられます。

  • エラーメッセージの文字列で分岐の比較をしているため、エラー情報に分岐のフラグ値以外の情報を含めることができません。
  • もし、他パッケージと文言が重複したら意図しない分岐になります。
  • 一度定義したエラー文言はフラグ値になるため、公開した時点で変更できなくなります。

方法2: カスタムエラーを用いた分岐(オススメ)

あなたが作っているアプリケーションで使う独自のエラー型(カスタムエラー)を作成して、詳細なエラー情報やエラーコード等を含められるようにします。それを使ってエラーハンドリングします。

カスタムエラーの作成

まず、独自のエラー型MyAppErrorを定義します。カスタムエラー型はError() stringを持つ必要があります。また、MyAppErrorはエラーコードを定義したErrorCodeを持ちます。
最後に生成関数としてNew()とカスタムエラーを抽出するAsMyAppErrorを定義しています。

errors/error.go
package errors

import (
    // Go言語オリジナルのerrorsパッケージはnativeに置換しております。
	native "errors"
	"fmt"
)

// アプリケーション独自のエラーコードを定義します。
type ErrorCode string

func (code ErrorCode) ToString() string {
	return string(code)
}

const (
	UserNotFound ErrorCode = "user not found errors"
	UserFatal    ErrorCode = "user fatal"
)

// エラー型を定義
type MyAppError struct {
	errorCode ErrorCode
	baseError error
}

func (e MyAppError) Error() string {
	if e.baseError == nil {
		return e.ErrorCode().ToString()
	}
	return fmt.Sprintf("%s %s", e.ErrorCode(), e.baseError.Error())
}

func (e MyAppError) ErrorCode() ErrorCode {
	return e.errorCode
}

// 生成関数
func New(e error, errorCode ErrorCode) error {
	return MyAppError{
		errorCode: errorCode,
		baseError: e,
	}
}

// カスタムコードの抽出関数
func AsMyAppError(err error) *MyAppError {
	var dst MyAppError
	if ok := native.As(err, &dst); ok {
		return &dst
	}
	return nil
}

これを実際に使用して見たいと思います。上記で定義した生成関数Newでエラーコードを渡してエラーを作成しています。
受け取ったerrorは汎用的なerror型ですので改めて、AsMyAppError関数を使ってカスタムエラーに変換しています。カスタムエラー型になればエラーコードが取得できるので、それに応じて分岐処理をしています。

main.go
func getUser(userID: Int) (*model.User, error) {
    // ユーザが見つからないエラーを返す
    return nil, errors.New(
		fmt.Errorf("ユーザが見つかりませんでした。"),
		errors.UserNotFound,
	)
}

func main() {
    user, err := getUser(2)

    dstErr := errors.AsMyAppError(err)
    if dstErr == nil {
        // アプリケーションのカスタムエラーが抽出できなかったので本当にヤバいエラー。
        panic("致命的なエラーが発生しました。" + err.Error())
    }
    // 以下、エラーコードに対する分岐を書く。
    switch dstErr.ErrorCode() {
	case errors.UserNotFound:
	case errors.UserFatal:
	}
}

最後のカスタムエラーを取得して、分岐処理の部分を、例えばHTTPサーバでレスポンス作成する部分に書けば、エラーコードに応じたHTTPステータスコードを返すと言ったことができるでしょう。

0
1
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
0
1