×
Copy
Open
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
エラー設計について / Designing Errors Go Conference 2019 Spring 2019/05/18
Slide 2
Slide 2 text
自己紹介 ● morikuni ● https://twitter.com/inukirom ● https://github.com/morikuni ● Mercari / Microservices Development ● Go & Application Architecture
Slide 3
Slide 3 text
アプリケーションのエラーハンドリングについて ● エラーとは? ● エラーに求められること ● Goのアプリケーションでエラーをどのように扱うか? Failure is your Domainという記事を参考にしています。 https://middlemost.com/failure-is-your-domain/ 本日の内容
Slide 4
Slide 4 text
エラーとは?
Slide 5
Slide 5 text
エラーとは? ● エラーは処理が失敗したときに発生する ● エラーには既知のエラーと未知のエラーの2種類がある ○ 既知のエラー ■ 発生することが想定できているエラー ■ 例: 明示的にハンドリングされているエラー ○ 未知のエラー ■ 発生することが想定できていないエラー ■ 例: panic, 500 Internal Server Errorになるようなエラー ● 同じエラーでも状況によって既知か未知かは異なる
Slide 6
Slide 6 text
エラーとは? どうやったら未知のエラーは既知になるのか? ● エラーをハンドリングし、自分達のアプリケーションの一部として組み込む ● 未知のエラーを既知とするための方法がエラーハンドリングである ● Goはエラーを明示的に返却するので本当に未知なのはpanicくらい ● 既知にもレベルがある ○ エラーが発生することを想定していない (panic) ○ エラーが発生することは知っているが詳細はわからない (error) ○ 具体的にどういう種類のエラーが発生するのかを知っている (sql.ErrNoRowsなど)
Slide 7
Slide 7 text
エラーに求められること
Slide 8
Slide 8 text
エラーに求められること ● エラーは、関係者に処理が失敗した原因を伝える必要がある ● 次のような関係者がそれぞれ異なる情報を求めている ○ アプリケーション ○ エンドユーザー ○ 運用者
Slide 9
Slide 9 text
アプリケーション ● エラーを識別できること(ハンドリングできること) ● Goにおいては次のいずれかのパターンで表現される(と思う) エラーに求めること var ErrNotFound = errors.New("not found") type NotFound struct{} type Error struct { Code string } type NotFoundError interface { NotFound() }
Slide 10
Slide 10 text
(クライアント)アプリケーション ● エラーを識別できること(ハンドリングできること) ● 多くの場合、通信プロトコルで定義されているエラー表現が使われる ○ HTTP Status Code ○ gRPC Status Code エラーに求めること
Slide 11
Slide 11 text
エンドユーザー ● 問題を解決するヒントになるようなメッセージが含まれていること ● メッセージはユーザーが理解できる形式であることが望ましい ○ 例: お金を引き出そうとしたときに残高が足りなかった場合 ■ Bad: 「CODE 19283757 invalid amount」 ■ Good: 「入力された金額が残高を上回っています。残高をご確認ください。」 エラーに求めること
Slide 12
Slide 12 text
運用者 ● 問題の根本的な原因を調査、解決する手助けになる情報が含まれていること ● エラーの識別情報やメッセージに加えてコールスタックやコンテキスト(引数など)も 表示し、エラーが発生した場所や状況が理解できるようになっていることが望ましい エラーに求めること
Slide 13
Slide 13 text
ここまでのまとめ
Slide 14
Slide 14 text
● エラーには既知のエラーと未知のエラーがある ● 未知のエラーを既知とする方法がエラーハンドリングである ● 関係者によってエラーに求める情報が異なる ○ エラーの識別情報 (for アプリケーション) ○ 人間が理解可能なメッセージ (for エンドユーザー) ○ コールスタックやコンテキスト (for 運用者) ここまでのまとめ
Slide 15
Slide 15 text
Goのアプリケーションで エラーをどのように扱うか?
Slide 16
Slide 16 text
Goのアプリケーションでエラーをどのように扱うか? ● アプリケーション固有のエラーコードを定義し、エラーを識別する ○ HTTPとかgRPCもエラーコードによる識別をしているので従う ● 既知の外部のエラーは上で定義したエラーコードに変換する ● 定義したエラーコードのみを想定してエラーハンドリングをする ○ 外部のエラーをそのまま使い回すと、想定すべきエラーが多くなり、アプリケーションのエ ラーハンドリングが複雑になる ● エンドユーザーがエラーを解決できる場合にメッセージを追加する ● 必要に応じてコールスタックや引数の情報を追加する ● 未知のエラーが見つかったらハンドリングして既知にする
Slide 17
Slide 17 text
github.com/morikuni/failure これらをアプリケーションで簡単に扱うために...
Slide 18
Slide 18 text
● エラーコードを中心にしたエラーハンドリング ● エラーメッセージ、コールスタック、コンテキスト情報の付与 ● Wrapper interfaceによるカスタマイズ性の高さ ● err.Error() の自動生成 github.com/morikuni/failure
Slide 19
Slide 19 text
failureを使ったエラーハンドリング
Slide 20
Slide 20 text
github.com/morikuni/failure-example /simple-czrud ● /create, /read, /update, /deleteの操作を持つ HTTPのKey-Value Store ● Controller, Service, Database, Modelという シンプルな構成 ● データベースにはMySQLを使用 failureを使ったエラーハンドリング
Slide 21
Slide 21 text
● アプリケーション固有のエラーコードを定義する failureを使ったエラーハンドリング package errors import ( "github.com/morikuni/failure" ) const ( InvalidArgument failure.StringCode = "InvalidArgument" NotFound failure.StringCode = "NotFound" AlreadyExist failure.StringCode = "AlreadyExist" )
Slide 22
Slide 22 text
● 既知の外部のエラーは定義したエラーコードに変換する ○ 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) }
Slide 23
Slide 23 text
● 定義したエラーコードのみを想定してエラーハンドリングをする ○ failure.Isでエラーコードで分岐できる failureを使ったエラーハンドリング if !failure.Is(err, errors.NotFound) { return failure.Wrap(err, context, ) }
Slide 24
Slide 24 text
● 定義したエラーコードのみを想定してエラーハンドリングをする ○ 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 } }
Slide 25
Slide 25 text
● エンドユーザーがエラーを解決できる場合にメッセージを追加する ○ 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."), ) }
Slide 26
Slide 26 text
● エンドユーザーがエラーを解決できる場合にメッセージを追加する ○ failure.MessageOfで付与したメッセージのみを取り出せる failureを使ったエラーハンドリング msg, ok := failure.MessageOf(err) if ok { io.WriteString(w, msg) } else { io.WriteString(w, http.StatusText(status)) }
Slide 27
Slide 27 text
● 必要に応じてコールスタックや引数の情報を追加する ○ 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 }
Slide 28
Slide 28 text
● 必要に応じてコールスタックや引数の情報を追加する ○ 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 // …(略)
Slide 29
Slide 29 text
● 未知のエラーが見つかったらハンドリングして既知にする ○ 運用をしているとエラーコードやメッセージが付いていないエラーが見つかる ■ failure.CodeOfやfailure.MessageOfの第2返り値がfalseになっているエラー ○ これらのエラーが未知のエラーである ○ 未知のエラーが見つかったら、エラーハンドリングをしてエラーコードやメッセージを付与 し、未知のエラーを既知のものとしていく failureを使ったエラーハンドリング
Slide 30
Slide 30 text
failureとxerrorsの考え方の違い
Slide 31
Slide 31 text
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() } } }
Slide 32
Slide 32 text
failureとxerrorsの考え方の違い failure xerrors エラーの比較 failure.Isによって最新のエラー コードを見る xerrors.Isによってエラーが含まれてい るか見る エラー情報の取り出し failure.MessageOfのように1 フィールド毎に関数を用意する xerrors.Asによってオブジェクトに情報 をマッピングする エラーのラップ方法 failure.Wrapper interfaceでラッピ ング方法が統一されている ラッパー毎にxerrors.Errorfなどの専用 の関数を用意する カスタマイズ性 Wrapperは1つずつ独立して使用 可能 挙動を変えるためには、新しい型を実 装する
Slide 33
Slide 33 text
failureとxerrorsの考え方の違い failureとxerrorsのどちらを使えばいいのか? ● アプリケーション(エンドユーザーに機能を提供する)ものであれば、failure ● ライブラリ(アプリケーションに組み込まれる)ものであれば、xerrors ライブラリでfailureを使うと、ライブラリのエラー情報とfailureを使うアプリケーションのエ ラー情報が混ざってしまって区別するのが困難になる (例: ライブラリのMessageがアプリケーションのエンドユーザーに出てしまう)
Slide 34
Slide 34 text
まとめ
Slide 35
Slide 35 text
● エラーには既知のエラーと未知のエラーがある ● 未知のエラーを既知とする方法がエラーハンドリングである ● 関係者によってエラーに求める情報が異なる ○ エラーの識別情報 (for アプリケーション) ○ 人間が理解可能なメッセージ (for エンドユーザー) ○ コールスタックやコンテキスト (for 運用者) ● アプリケーション内のエラーを定義し、自分達でエラーを管理しよう ● github.com/morikuni/failure オススメです まとめ