Share
Embed
エラー設計について / Designing Errors
アプリケーションのエラーハンドリングについて
- エラーとは?
- エラーに求められること
- Goのアプリケーションでエラーをどのように扱うか?
Failure is your Domainという記事を参考にしています。
https://middlemost.com/failure-is-your-domain/
Google Presentation版
https://docs.google.com/presentation/d/1JIdZ4IVW2D3kEFUtWSvHNes3r3ykojGuUAQAnhmEVs0/edit?usp=sharing
morikuni
May 18, 2019
Transcript
35 slides
-
自己紹介 • morikuni • https://twitter.com/inukirom • https://github.com/morikuni • Mercari /
Microservices Development • Go & Application Architecture -
アプリケーションのエラーハンドリングについて • エラーとは? • エラーに求められること • Goのアプリケーションでエラーをどのように扱うか? Failure is your
Domainという記事を参考にしています。 https://middlemost.com/failure-is-your-domain/ 本日の内容 -
エラーとは? • エラーは処理が失敗したときに発生する • エラーには既知のエラーと未知のエラーの2種類がある ◦ 既知のエラー ▪ 発生することが想定できているエラー ▪
例: 明示的にハンドリングされているエラー ◦ 未知のエラー ▪ 発生することが想定できていないエラー ▪ 例: panic, 500 Internal Server Errorになるようなエラー • 同じエラーでも状況によって既知か未知かは異なる -
エラーとは? どうやったら未知のエラーは既知になるのか? • エラーをハンドリングし、自分達のアプリケーションの一部として組み込む • 未知のエラーを既知とするための方法がエラーハンドリングである • Goはエラーを明示的に返却するので本当に未知なのはpanicくらい • 既知にもレベルがある
◦ エラーが発生することを想定していない (panic) ◦ エラーが発生することは知っているが詳細はわからない (error) ◦ 具体的にどういう種類のエラーが発生するのかを知っている (sql.ErrNoRowsなど) -
アプリケーション • エラーを識別できること(ハンドリングできること) • Goにおいては次のいずれかのパターンで表現される(と思う) エラーに求めること var ErrNotFound = errors.New("not
found") type NotFound struct{} type Error struct { Code string } type NotFoundError interface { NotFound() } -
(クライアント)アプリケーション • エラーを識別できること(ハンドリングできること) • 多くの場合、通信プロトコルで定義されているエラー表現が使われる ◦ HTTP Status Code ◦
gRPC Status Code エラーに求めること -
エンドユーザー • 問題を解決するヒントになるようなメッセージが含まれていること • メッセージはユーザーが理解できる形式であることが望ましい ◦ 例: お金を引き出そうとしたときに残高が足りなかった場合 ▪ Bad:
「CODE 19283757 invalid amount」 ▪ Good: 「入力された金額が残高を上回っています。残高をご確認ください。」 エラーに求めること -
• エラーには既知のエラーと未知のエラーがある • 未知のエラーを既知とする方法がエラーハンドリングである • 関係者によってエラーに求める情報が異なる ◦ エラーの識別情報 (for アプリケーション)
◦ 人間が理解可能なメッセージ (for エンドユーザー) ◦ コールスタックやコンテキスト (for 運用者) ここまでのまとめ -
Goのアプリケーションでエラーをどのように扱うか? • アプリケーション固有のエラーコードを定義し、エラーを識別する ◦ HTTPとかgRPCもエラーコードによる識別をしているので従う • 既知の外部のエラーは上で定義したエラーコードに変換する • 定義したエラーコードのみを想定してエラーハンドリングをする ◦
外部のエラーをそのまま使い回すと、想定すべきエラーが多くなり、アプリケーションのエ ラーハンドリングが複雑になる • エンドユーザーがエラーを解決できる場合にメッセージを追加する • 必要に応じてコールスタックや引数の情報を追加する • 未知のエラーが見つかったらハンドリングして既知にする -
• エラーコードを中心にしたエラーハンドリング • エラーメッセージ、コールスタック、コンテキスト情報の付与 • Wrapper interfaceによるカスタマイズ性の高さ • err.Error() の自動生成
github.com/morikuni/failure -
github.com/morikuni/failure-example /simple-czrud • /create, /read, /update, /deleteの操作を持つ HTTPのKey-Value Store •
Controller, Service, Database, Modelという シンプルな構成 • データベースにはMySQLを使用 failureを使ったエラーハンドリング -
• アプリケーション固有のエラーコードを定義する failureを使ったエラーハンドリング package errors import ( "github.com/morikuni/failure" ) const
( InvalidArgument failure.StringCode = "InvalidArgument" NotFound failure.StringCode = "NotFound" AlreadyExist failure.StringCode = "AlreadyExist" ) -
• 既知の外部のエラーは定義したエラーコードに変換する ◦ failure.Translateでエラーにエラーコードを付与できる failureを使ったエラーハンドリング const query = ` SELECT
v FROM kv WHERE k = ? ` r := db.conn.QueryRowContext(ctx, query, key) var i int64 if err := r.Scan(&i); err != nil { if err == sql.ErrNoRows { return 0, failure.Translate(err, errors.NotFound) } return 0, failure.Wrap(err) } -
• 定義したエラーコードのみを想定してエラーハンドリングをする ◦ failure.Isでエラーコードで分岐できる failureを使ったエラーハンドリング if !failure.Is(err, errors.NotFound) { return
failure.Wrap(err, context, ) } -
• 定義したエラーコードのみを想定してエラーハンドリングをする ◦ failure.CodeOfでエラーコードを取り出せる failureを使ったエラーハンドリング func httpStatus(err error) int {
switch c, _ := failure.CodeOf(err); c { case errors.InvalidArgument: return http.StatusBadRequest case errors.NotFound: return http.StatusNotFound case errors.AlreadyExist: return http.StatusConflict default: return http.StatusInternalServerError } } -
• エンドユーザーがエラーを解決できる場合にメッセージを追加する ◦ failure.Messageをfailure.Newやfailure.Translateのオプションとして渡せる failureを使ったエラーハンドリング _, err := s.db.Get(ctx, key)
if err == nil { return failure.New(errors.AlreadyExist, failure.Message("Specified key already exists. Use update for existing key."), ) } -
• エンドユーザーがエラーを解決できる場合にメッセージを追加する ◦ failure.MessageOfで付与したメッセージのみを取り出せる failureを使ったエラーハンドリング msg, ok := failure.MessageOf(err) if
ok { io.WriteString(w, msg) } else { io.WriteString(w, http.StatusText(status)) } -
• 必要に応じてコールスタックや引数の情報を追加する ◦ failure.Newやfailure.Wrapを使うと自動でコールスタックが追加される failureを使ったエラーハンドリング func (s *service) Create(ctx context.Context,
key model.Key, value model.Value) error { context := failure.Context{"key": string(key)} ... if err != nil { return failure.Wrap(err, context, ) } return nil } -
• 必要に応じてコールスタックや引数の情報を追加する ◦ err.Error()などが自動で生成される failureを使ったエラーハンドリング c.logger.Printf("%v\n", err) // controller.(*Controller).create: service.(*service).Create:
Specified key already exists. Use update for existing key.: key=a: code(AlreadyExist) c.logger.Printf("%+v\n", err) // [controller.(*Controller).create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/controller/controller.go:76 // [service.(*service).Create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/service/service.go:32 // message("Specified key already exists. Use update for existing key.") // key = a // code(AlreadyExist) // [CallStack] // [service.(*service).Create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/service/service.go:32 // [controller.(*Controller).create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/controller/controller.go:74 // …(略) -
failureとxerrorsの考え方の違い • failureはerror型にOptionalフィールドを足してエラーを拡張するイメージ ◦ 1つのWrapperが1つのフィールドや機能を追加する • xerrorsはエラーを変換していくイメージ type Error interface
{ Error() string ErrorCode() (Code, bool) CallStack() (CallStack, bool) Message() (string, bool) // other optional fields... } AppError{ URLError{ SyscallError{ Errno() } } } -
failureとxerrorsの考え方の違い failure xerrors エラーの比較 failure.Isによって最新のエラー コードを見る xerrors.Isによってエラーが含まれてい るか見る エラー情報の取り出し failure.MessageOfのように1
フィールド毎に関数を用意する xerrors.Asによってオブジェクトに情報 をマッピングする エラーのラップ方法 failure.Wrapper interfaceでラッピ ング方法が統一されている ラッパー毎にxerrors.Errorfなどの専用 の関数を用意する カスタマイズ性 Wrapperは1つずつ独立して使用 可能 挙動を変えるためには、新しい型を実 装する -
• エラーには既知のエラーと未知のエラーがある • 未知のエラーを既知とする方法がエラーハンドリングである • 関係者によってエラーに求める情報が異なる ◦ エラーの識別情報 (for アプリケーション)
◦ 人間が理解可能なメッセージ (for エンドユーザー) ◦ コールスタックやコンテキスト (for 運用者) • アプリケーション内のエラーを定義し、自分達でエラーを管理しよう • github.com/morikuni/failure オススメです まとめ