Goのエラーハンドリングの基本的な考え方
はじめに
アプリケーションの開発においてエラーハンドリングの設計は重要です。エラーハンドリングの設計を軽視すると、エラー発生時に原因が明確にわからないことや、エンドユーザーに対して不親切なエラーメッセージを返すことになってしまってもおかしくはありません。
本記事ではGoのエラーハンドリングの基本的な考え方について紹介します。
エラーについて
エラーはプログラムが異常な処理をした場合に発生するもので、Ben Johnson氏の記事によるとエラーは以下の2つに分けることができます。
- well-defined error(明確なエラー)
- ハンドリングできているエラーで、アプリケーション内で予測できているエラー
- undefined error(未定義のエラー)
- panicなどアプリケーションで予測できていないエラー
エラーはただ返せば良いわけではない
エラーは処理に失敗したときにerr
をただ表示するだけではエラーハンドリングを十分に行えているとは言えません。エラーを扱うシーンによって、エラーに含める情報やエラーハンドリングを工夫する必要があります。エラーの利用シーンは主に以下の3つに分けられ、それぞれのシーンでエラーの扱い方は異なります。
- アプリケーション内でエラーを扱うとき
- エラーにアプリケーション固有のエラーコードを加えることで、エラーを識別しエラーハンドリング可能な状態にする
- エンドユーザーがエラーをみるとき
- エラーに人間が読んでわかるメッセージを含めて、ユーザーがエラーメッセージを見て行動を取れる状態にする
- オペレーターがエラーをみて障害対応などをするとき
- エラーにオペレーションの情報を含めてスタックしておくことで、システムの運用者がエラーの発生原因を特定しやすくする
サンプル
実際にどのようにエラーを実装していけばよいのかを擬似コードをもとに説明していきます。
エラーの定義例
先ほどのエラーの利用シーンに対応するためには、エラーに以下の情報を含める必要があります。
- アプリケーション固有のエラーコード
- エンドユーザー向けに表示するメッセージ
- 発生源のエラーやラップされたエラーを格納するエラー
コードにすると以下のようになります。ApplicationError
はエラーコードの他に、Error
メソッドを持っておりerror
インターフェースを満たすようになっています。
type ApplicationError struct { // Code is ApplicationErrorCode Code string // Message is to show enduser Message string // Err is nested error Err error } // Error is implementation for error interface func (e *ApplicationError) Error() string { errStr := "" if e.Err != nil { errStr := fmt.Sprintf("%s", e.Err.Error()) } return fmt.Sprintf("code=%s, err=%s, msg=%s", e.Code, errStr, e.Message) }
UserService.FindByID
のエラー変換の実装例
今回は仮の実装としてUserService.FindByID
から返されるエラーを内部のエラーに変換してみます。まずは、以下のようなアプリケーション固有の内部のエラーコードを定義します。内部のエラーコードは、外部サービスやライブラリのエラーコードを、アプリケーション固有のエラーコードとして扱うために利用されます。アプリケーションでは内部のエラーコードを利用してエラーハンドリングを行います。
// ApplicationErrorCode const( InternalServerErrorCode = "InternalServerError" NotFoundErrorCode = "NotFoundError" InvalidArgumentErrorCode = "InvalidArgumentError" )
以下のコードではUserService.client.Get
のエラーを場合に応じて内部のエラーコードに変換しています。エラーが有る場合はgRPCのステータスコードがあるかないかをチェックして、内部のエラーコードであるInternalServerErrorCode
やNotFoundErrorCode
に変換しています。NotFoundErrorCode
の場合はエンドユーザー向けに人間が理解可能なメッセージを付与しています。InternalServerError
の場合はエラーをpkg/errors
などでラップして詳細な情報を加えることで、システムの運用者がエラーの原因を追いやすくなるようにしています。
func (s *UserService) FindByID(id string) (*User, error) { res, err := s.client.Get(id) if err != nil { // err to gRPC status status, ok := status.FromError(err) if !ok { return nil, &apperr.ApplicationError{Code: apperr.InternalServerErrorCode, Err: errors.Wrapf(err, "grpc connection is failed")} } // external error to internal error switch status.Code() { case codes.NotFound: // respond err message to enduser return nil, &apperr.ApplicationError{Code: apperr.NotFoundErrorCode, Message: "ユーザーが見つかりませんでした", Err: err} case default: // wrap original error and add detail information return nil, &apperr.ApplicationError{Code: apperr.InternalServerErrorCode, Err: errors.Wrapf(err, "UserService.Get() responds error: id=%s", id)} } } return &User{ ID: res.userID, Name: res.name, }, nil }
固有エラーからgRPCステータスコードへのエラーハンドリング例
この実装例では内部のエラーコードをgRPCのステータスーコードに変換しています。err.(apperr.ApplicationError)
により、定義したアプリケーション内部のエラーコードのみでエラーハンドリングを行います。外部のエラーを内部のエラーに変換しておくことで、最終的にエラーハンドリングする際に懸念事項をぐっと減らすことができます。
func grpcStatus(err error) codes.Code { if e, ok := err.(apperr.ApplicationError); ok { switch e.Code { case domainerr.InternalServerErrCode: return codes.Internal case apperr.NotFoundErrorCode: return codes.NotFound case apperr.InvalidArgumentErrorCode: return codes.InvalidArgument } } return codes.Internal }
まとめ
今回はエラーハンドリングについて例を踏まえてまとめました。これは基本的なエラーの設計方針にとどまっているため、実際の実装はシステムによって様々です。またエラーについて記事を書くことがあれば、実際のアプリケーションでエラーハンドリング設計から実装までを紹介できればと思います。