Gin基礎 #4 レスポンス処理 — JSON・ステータスコード・エラー
前回の記事ではリクエストを受け取って検証する方法を扱いました。今回はその逆方向、つまりレスポンスを作る方法を整理します。JSON以外のレスポンス形式、ステータスコードの扱い方、そしてAPI全体が一貫したエラー形式を持つようにするパターンまで見ていきます。
- #1 はじめてのサーバー
- #2 ルーティングとハンドラ
- #3 リクエストバインディングと検証
- #4 レスポンス処理 — JSON・ステータスコード・エラー ← 今回の記事
- #5 ミドルウェア
- #6 データベース連携 (GORM)
- #7 プロジェクト構成とミニREST API
JSONレスポンスを改めて見る #
最もよく使うレスポンスはJSONです。c.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) // YAMLc.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": "認証が必要です"})
returnAbort 系統とミドルウェアの関係は #5 ミドルウェア で詳しく扱います。
まとめ #
今回の記事で整理した内容:
- JSONは
c.JSON、構造体のomitemptyで空のフィールドを隠せる - ステータスコードは
http.StatusCreatedのように標準定数で、意味に合わせて選ぶ - 本文のないレスポンスは
c.Status、文字列はc.String、バイトはc.Data - ファイルは
c.File、ダウンロードはc.FileAttachment - リダイレクトは
c.Redirect、ヘッダはc.Header - エラーレスポンスは形式を一つに決め、ヘルパーで統一する
- リクエストをすぐに終わらせるときは
c.AbortWithStatusJSON
次の記事(#5 ミドルウェア)では、ロギング、リカバリ、認証のように複数のハンドラに共通でかける処理をミドルウェアにまとめる方法を整理します。