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 オススメです まとめ