Gin基礎 #4 レスポンス処理 — JSON・ステータスコード・エラー

読了 5分

前回の記事ではリクエストを受け取って検証する方法を扱いました。今回はその逆方向、つまりレスポンスを作る方法を整理します。JSON以外のレスポンス形式、ステータスコードの扱い方、そしてAPI全体が一貫したエラー形式を持つようにするパターンまで見ていきます。

  • #1 はじめてのサーバー
  • #2 ルーティングとハンドラ
  • #3 リクエストバインディングと検証
  • #4 レスポンス処理 — JSON・ステータスコード・エラー ← 今回の記事
  • #5 ミドルウェア
  • #6 データベース連携 (GORM)
  • #7 プロジェクト構成とミニREST API

JSONレスポンスを改めて見る #

最もよく使うレスポンスはJSONです。c.JSON はステータスコードとデータを受け取ります。

JSONレスポンス
c.JSON(http.StatusOK, gin.H{"message": "ok"})

構造体をそのまま渡しても構いません。json タグに従ってシリアライズされます。

構造体レスポンス
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
}

c.JSON(http.StatusOK, User{ID: 1, Name: "カーティス"})
// {"id":1,"name":"カーティス"}

omitempty を付けた Email は値が空なので出力から外れました。レスポンスで空のフィールドを隠したいときによく使います。

インデントされたJSONが必要なら c.IndentedJSON を、HTML特殊文字をエスケープせずそのまま出力したいなら c.PureJSON を使います。ただし本番APIでは容量を減らすため、通常は c.JSON をそのまま使います。

ステータスコード #

ステータスコードは数値の代わりに net/http の定数を使うほうが読みやすいです。

よく使うステータスコード
http.StatusOK                  // 200
http.StatusCreated             // 201
http.StatusNoContent           // 204
http.StatusBadRequest          // 400
http.StatusUnauthorized        // 401
http.StatusForbidden           // 403
http.StatusNotFound            // 404
http.StatusInternalServerError // 500

リソースを新しく作ったなら 201 Created、削除のように返す本文がなければ 204 No Content を使うというように、意味に合わせて選びます。

本文のないレスポンス
c.Status(http.StatusNoContent) // 204, 本文なし

JSON以外のレスポンス形式 #

JSONだけがレスポンスではありません。状況に応じていくつかの形式を使えます。

文字列と生データ
c.String(http.StatusOK, "こんにちは, %s", name) // プレーンテキスト
c.Data(http.StatusOK, "image/png", bytes)       // バイトそのまま
c.XML(http.StatusOK, data)                       // XML
c.YAML(http.StatusOK, data)                      // YAML

c.String はフォーマット文字列を受け取り、fmt.Sprintf のように動作します。c.Data はContent-Typeとバイトスライスを直接渡し、画像やPDFのような任意のバイナリをレスポンスするときに使います。

ファイルレスポンス #

ファイルをそのまま返すには c.File を使います。

ファイルレスポンス
c.File("./static/report.pdf")

ブラウザがダウンロードするようファイル名を指定するには c.FileAttachment を使います。

ダウンロードレスポンス
c.FileAttachment("./static/report.pdf", "monthly-report.pdf")

こうすると Content-Disposition ヘッダが設定され、ブラウザが指定した名前で保存ダイアログを開きます。

リダイレクト #

別のURLへ送るには c.Redirect を使います。

リダイレクト
c.Redirect(http.StatusFound, "/login") // 302

恒久的な移動なら http.StatusMovedPermanently(301)を使います。

ヘッダ設定 #

レスポンスヘッダは c.Header で直接指定します。

ヘッダ設定
c.Header("X-Request-ID", requestID)
c.Header("Cache-Control", "no-store")

一貫したエラーレスポンスを作る #

APIが大きくなると、エラーレスポンスの形式がハンドラごとにばらばらになりがちです。あるところは {"error": "..."}、あるところは {"message": "..."} というように分かれると、クライアントは処理しづらくなります。形式を一つに決めておくほうがよいです。

まずエラーレスポンスの構造体とヘルパーを定義します。

エラーレスポンスヘルパー
type ErrorResponse struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func respondError(c *gin.Context, status int, code, message string) {
	c.JSON(status, ErrorResponse{Code: code, Message: message})
}

ハンドラではこのヘルパーだけを呼び出します。

ヘルパーの使用
func getUser(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		respondError(c, http.StatusBadRequest, "INVALID_ID", "idは数値でなければなりません")
		return
	}

	user, found := findUser(id)
	if !found {
		respondError(c, http.StatusNotFound, "USER_NOT_FOUND", "ユーザーが見つかりません")
		return
	}

	c.JSON(http.StatusOK, user)
}

こうするとすべてのエラーレスポンスが同じ形を持ちます。クライアントは code の値で分岐し、message をユーザーに見せられます。中級シリーズではこのパターンをミドルウェアベースの中央エラー処理へと発展させます。

Abort — 処理の中断 #

ミドルウェアの中で、またはハンドラの冒頭でリクエストをすぐに終わらせる必要があるときがあります。c.AbortWithStatusJSON はレスポンスを送りつつ、以降のハンドラチェーンの実行を止めます。

中断しつつレスポンス
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "認証が必要です"})
return

Abort 系統とミドルウェアの関係は #5 ミドルウェア で詳しく扱います。

まとめ #

今回の記事で整理した内容:

  • JSONは c.JSON、構造体の omitempty で空のフィールドを隠せる
  • ステータスコードは http.StatusCreated のように標準定数で、意味に合わせて選ぶ
  • 本文のないレスポンスは c.Status、文字列は c.String、バイトは c.Data
  • ファイルは c.File、ダウンロードは c.FileAttachment
  • リダイレクトは c.Redirect、ヘッダは c.Header
  • エラーレスポンスは形式を一つに決め、ヘルパーで統一する
  • リクエストをすぐに終わらせるときは c.AbortWithStatusJSON

次の記事(#5 ミドルウェア)では、ロギング、リカバリ、認証のように複数のハンドラに共通でかける処理をミドルウェアにまとめる方法を整理します。

X