この文章について
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
を定義しています。
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関数を使ってカスタムエラーに変換しています。カスタムエラー型になればエラーコードが取得できるので、それに応じて分岐処理をしています。
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ステータスコードを返すと言ったことができるでしょう。