🐱
Go - コントローラ層でのエラーハンドリング
記念すべきZenn1投稿目。Goについてお話したいと思います。
Goでのエラーハンドリングっていろいろ悩まない?
最近Goに触れる機会が増えてきたのですが、JVM言語がメインだった人間からすると
なかなかとっつきにくいところが多く、慣れない日々です。そのひとつがエラーハンドリングです。
特に、API実装などで「ドメイン層で発生したエラーをコントローラ層でどうハンドリングしようかな...」という。
そこで自分なりに「これが一番すっきりしたかな」と思うシグネチャを紹介します。
まずはよくあるパターン
books_controller.go
func (c *booksController) Update(ctx *gin.Context) {
req := &BooksApiUpdateRequest{}
if err := ctx.ShouldBind(req); err != nil {
// リクエストが不正ならBadRequestを返す
ctx.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
ctx.Abort()
return
}
// ここでの戻り値errは独自定義したエラー(共通エラーコードなどを持つ)
result, err := c.BooksUseCase.Update(req.Id, req.Name)
if err != nil {
switch err.Code {
case errors.NotFound:
// Bookが見つからなかったらNotFoundを返す
ctx.JSON(http.StatusNotFound, gin.H{"msg": err.Msg()})
ctx.Abort()
return
case errors.InternalError:
...
}
}
ctx.JSON(http.StatusOK, gin.H{"msg": "success", "result": result})
}
エラーが発生する可能性があるたびにその場でエラー内容をチェックして
エラーレスポンスを生成する...ってのがどうしてもカッコ悪いな〜と。
そこで考えたのがこれ
「せっかく独自エラー定義したんだし、もっと上位層でハンドリングしたほうがよくない?」
books_controller.go
func (c *booksController) Update(ctx *gin.Context) (interface{}, *errors.Error) {
req := &BooksApiUpdateRequest{}
if err := ctx.ShouldBind(req); err != nil {
// 独自エラーへ変換
return nil, errors.NewError(errors.InvalidRequest, err)
}
return c.BooksUseCase.Update(req.Id, req.Name)
}
router.go
func init() {
router := gin.Default()
...
router.PUT("/api/v1/books", handler(booksController.Update))
...
}
// コントローラでの実行結果(=ユースケースでの実行結果)をそのまま受け取りレスポンスハンドリングさせる
func handler(fn func(c *gin.Context) (interface{}, *errors.Error)) gin.HandlerFunc {
return func(c *gin.Context) {
result, err := fn(c)
if err != nil {
c.JSON(toHttpStatusCode(err.Code), gin.H{"msg": err.Msg()})
c.Abort()
} else {
c.JSON(http.StatusOK, gin.H{"msg": "success", "result": result})
}
}
}
// 独自エラーコードに基づいてHttpレスポンスコードに変換する
func toHttpStatusCode(code errors.ErrorCode) int {
switch code {
case errors.InvalidRequest:
return http.StatusBadRequest
case errors.NotFound:
return http.StatusNotFound
case errors.InternalError:
...
}
}
高階関数をrouter側に定義することによって、
- コントローラ層での実装を限りなくシンプルに保てる
- ユニテが書きやすい
- 新規API実装時にいちいちハンドリングを考慮・書く必要がなくなる
- 他APIとの整合性も簡単に保てる
- エラーコードも簡単に追加できる
- 独自コンテキストを定義すればginをコントローラ層から追い出せる
- 技術(フレームワーク)をrouterに集中させることができる
といったメリットがあるかなと思います。クリーンアーキテクチャなどとも相性がよいかと。
最後に
実装するアプリケーションの規模感にもよってくるところはあるかと思いますが、
なにかヒントになれば幸いです。ではまた。
Discussion