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

# この文章について

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`かどうかを比較して分岐しています。

```go
// ユーザが見つからないエラーを定義
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()`を使うようですね。

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

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

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


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

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

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

```go: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関数を使ってカスタムエラーに変換しています。カスタムエラー型になればエラーコードが取得できるので、それに応じて分岐処理をしています。

```go: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ステータスコードを返すと言ったことができるでしょう。