🐱

Go - コントローラ層でのエラーハンドリング

2021/12/01に公開

記念すべき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