Gin基礎 #5 ミドルウェア

読了 5分

前回の記事では応答処理を扱いました。今回の記事はミドルウェアです。ロギング、リカバリー、認証のように複数のハンドラに共通で必要な処理を、ハンドラごとに繰り返さず一か所にまとめる方法です。

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

ミドルウェアとは? #

ミドルウェアは、ハンドラが実行される前後に割り込む関数です。リクエストが入ってくるとミドルウェアを順番に通ってハンドラに到達し、応答が出ていくときにその逆順で再びミドルウェアを通ります。

リクエストの流れ
リクエスト → ミドルウェアA → ミドルウェアB → ハンドラ → ミドルウェアB → ミドルウェアA → 応答

この構造のおかげで、「すべてのリクエストにログを残す」「特定のグループは認証を先に確認する」のような共通処理を、ハンドラ本体と切り離せます。

すでに使っていたミドルウェア — Logger、Recovery #

#1では gin.Default() でルーターを作りました。このとき2つのミドルウェアが自動で付きます。

  • Logger — リクエストごとにメソッド、パス、ステータスコード、処理時間をコンソールに出力
  • Recovery — ハンドラでpanicが起きてもサーバーを落とさず500で応答

gin.New() で作ると、この2つがない空のエンジンになります。必要なミドルウェアを自分で付けたいときはこう始めます。

自分で構成する
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())

r.Use がミドルウェアをエンジン全体に登録する関数です。

カスタムミドルウェアを作る #

ミドルウェアはハンドラと同じ gin.HandlerFunc 型です。つまり func(c *gin.Context) の形ならよいのです。リクエストの処理時間を計測するミドルウェアを作ってみます。

処理時間計測ミドルウェア
func Timer() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()

		c.Next() // 次のミドルウェアとハンドラを実行

		elapsed := time.Since(start)
		log.Printf("%s %s — %v", c.Request.Method, c.Request.URL.Path, elapsed)
	}
}

核心は c.Next() です。この呼び出しを基準に、上側はハンドラ実行の、下側はに動きます。c.Next() で次の段階を実行したあと、再び戻ってきて経過時間を出力します。

登録は他のミドルウェアと同じです。

登録
r := gin.Default()
r.Use(Timer())

ミドルウェアを関数で一度包む理由は、登録の時点で設定値を受け取れるようにするためです。たとえば Timer(threshold) のように引数を受け取り、クロージャに閉じ込めておけます。

c.Next() と c.Abort() #

ミドルウェアで流れを制御する2つのメソッドです。

  • c.Next() — 次のミドルウェアとハンドラへ進む
  • c.Abort() — 以降の段階を実行せず中断する

Abort は応答を送りません。そのため、通常は応答と一緒に使う c.AbortWithStatusJSON を活用します。

条件によって中断
func RequireAPIKey() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.GetHeader("X-API-Key") != "secret" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "無効なキー"})
			return
		}
		c.Next()
	}
}

キーが間違っていれば401で応答し、return でミドルウェア関数を終えます。このとき Abort がかかっているので、後続のハンドラは実行されません。キーが正しければ c.Next() で通過させます。

適用範囲 — グローバル、グループ、ルート #

ミドルウェアは、かける位置によって適用範囲が変わります。

範囲別の適用
r := gin.Default()

// 1) グローバル — すべてのルート
r.Use(Timer())

// 2) グループ — グループ内のルートだけ
admin := r.Group("/admin", RequireAPIKey())
{
	admin.GET("/stats", showStats)
}

// 3) ルート — そのルート1つだけ
r.GET("/profile", RequireAPIKey(), showProfile)

#2で見たルーターグループと組み合わせると、認証が必要なエンドポイントだけを1つのグループにまとめ、ミドルウェアを一度にかける構造が自然に出てきます。公開APIと保護されたAPIをグループで分ける方式です。

ミドルウェアからハンドラへ値を渡す #

ミドルウェアが計算した値を、後続のハンドラで使いたいときがあります。認証ミドルウェアが突き止めたユーザー情報が代表的です。c.Set で保存し c.Get で取り出します。

コンテキストに値を保存
func Auth() gin.HandlerFunc {
	return func(c *gin.Context) {
		userID := 42 // 実際にはトークンから抽出
		c.Set("userID", userID)
		c.Next()
	}
}

func profile(c *gin.Context) {
	userID, exists := c.Get("userID")
	if !exists {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "認証が必要"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"userID": userID})
}

c.Get は値と存在の有無を一緒に返します。保存された値の型は any なので、取り出すときに型アサーションが必要になることがあります。型を指定して取り出す c.GetIntc.GetString のようなヘルパーもあります。

型指定ヘルパー
userID := c.GetInt("userID")

本格的なJWTベースの認証は、中級シリーズでこのパターンの上に積み上げていきます。

よく使う外部ミドルウェア #

自分で作らなくてもよい一般的な処理には、公式またはコミュニティのミドルウェアがあります。

  • CORSgithub.com/gin-contrib/cors
  • gzip圧縮github.com/gin-contrib/gzip
  • セッションgithub.com/gin-contrib/sessions

インストール後に r.Use で登録する方式は同じです。CORSやレートリミットのような運用向けミドルウェアは、中級シリーズで改めて扱います。

まとめ #

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

  • ミドルウェアはハンドラの前後に割り込む func(c *gin.Context) 関数
  • gin.Default() はLoggerとRecoveryを自動で含む
  • 登録は r.Use、カスタムミドルウェアは通常クロージャで包んで設定値を受け取る
  • c.Next() を基準にハンドラ実行の前後が分かれる
  • c.Abort 系で以降の段階を止め、認証ゲートを実装
  • グローバル、グループ、ルート単位で適用範囲を決められる
  • ミドルウェアとハンドラの間の値の受け渡しは c.Set / c.Get

次の記事(#6 データベース連携)では、GORMを付けて実際のデータを扱うCRUD APIを作ります。

X