エラー設計について / Designing Errors
Go Conference 2019 Spring
2019/05/18
自己紹介
本日の内容
アプリケーションのエラーハンドリングについて
Failure is your Domainという記事を参考にしています。�https://middlemost.com/failure-is-your-domain/
エラーとは?
エラーとは?
エラーとは?
どうやったら未知のエラーは既知になるのか?
エラーに求められること
エラーに求められること
エラーに求めること
アプリケーション
var ErrNotFound = errors.New("not found")
type NotFound struct{}
type Error struct {
Code string
}
type NotFoundError interface {
NotFound()
}
エラーに求めること
(クライアント)アプリケーション
エラーに求めること
エンドユーザー
エラーに求めること
運用者
ここまでのまとめ
ここまでのまとめ
Goのアプリケーションで�エラーをどのように扱うか?
Goのアプリケーションでエラーをどのように扱うか?
github.com/morikuni/failure
これらをアプリケーションで簡単に扱うために...
github.com/morikuni/failure
failureを使ったエラーハンドリング
failureを使ったエラーハンドリング
github.com/morikuni/failure-example�/simple-crud
failureを使ったエラーハンドリング
package errors
import (
"github.com/morikuni/failure"
)
const (
InvalidArgument failure.StringCode = "InvalidArgument"
NotFound failure.StringCode = "NotFound"
AlreadyExist failure.StringCode = "AlreadyExist"
)
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を使ったエラーハンドリング
if !failure.Is(err, errors.NotFound) {
return failure.Wrap(err,
context,
)
}
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を使ったエラーハンドリング
_, 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を使ったエラーハンドリング
msg, ok := failure.MessageOf(err)
if ok {
io.WriteString(w, msg)
} else {
io.WriteString(w, http.StatusText(status))
}
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
}
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を使ったエラーハンドリング
failureとxerrorsの考え方の違い
failureと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つずつ独立して使用可能 | 挙動を変えるためには、新しい型を実装する |
failureとxerrorsの考え方の違い
failureとxerrorsのどちらを使えばいいのか?
ライブラリでfailureを使うと、ライブラリのエラー情報とfailureを使うアプリケーションのエラー情報が混ざってしまって区別するのが困難になる
(例: ライブラリのMessageがアプリケーションのエンドユーザーに出てしまう)
まとめ
まとめ