Gin基礎 #2 ルーティングとハンドラ

読了 5分

前回の記事で最初のGinサーバーを立ち上げました。今回の記事はルーティングの基礎を整理します。HTTPメソッドごとのルート登録、パスパラメータとクエリストリングの読み取り、そしてルーターグループでエンドポイントをまとめる方法まで扱います。

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

HTTPメソッドごとのルート登録 #

Ginはメソッドごとに対応する関数を提供します。同じパスでもメソッドが違えば、別々のハンドラを登録できます。

メソッドごとのルート
r := gin.Default()

r.GET("/users", listUsers)
r.POST("/users", createUser)
r.GET("/users/:id", getUser)
r.PUT("/users/:id", updateUser)
r.PATCH("/users/:id", patchUser)
r.DELETE("/users/:id", deleteUser)

このようにメソッドとパスの組み合わせでエンドポイントを定義します。REST APIの基本的な骨格がそのまま表れます。ハンドラを匿名関数ではなく、名前付きの関数として分離している点に注目してください。エンドポイントが増えると、ハンドラを関数に切り出すほうがずっと読みやすくなります。

パスパラメータ — :名前 #

パスの一部を変数として受け取るには、コロンを付けます。c.Param で値を読みます。

パスパラメータ
r.GET("/users/:id", func(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{"userID": id})
})
リクエスト
curl http://localhost:8080/users/42
# {"userID":"42"}

c.Param が返す値は常に文字列です。数値として扱う必要があれば、strconv.Atoi などで自分で変換します。

数値変換
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
	c.JSON(http.StatusBadRequest, gin.H{"error": "idは数値でなければなりません"})
	return
}

パラメータは複数使うこともできます。

複数パラメータ
r.GET("/users/:id/posts/:postID", func(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"userID": c.Param("id"),
		"postID": c.Param("postID"),
	})
})

ワイルドカード — *名前 #

パスの残り全体を受け取りたいときは、アスタリスクを使います。ファイルパスのようにスラッシュが複数入る値を受け取るときに便利です。

ワイルドカード
r.GET("/files/*filepath", func(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"path": c.Param("filepath")})
})
リクエスト
curl http://localhost:8080/files/images/logo.png
# {"path":"/images/logo.png"}

ワイルドカードで受け取った値は、先頭にスラッシュが付いています。

クエリストリング — ?key=value #

URLの後ろに付くクエリストリングは c.Query で読みます。

クエリの読み取り
r.GET("/search", func(c *gin.Context) {
	keyword := c.Query("q")
	c.JSON(http.StatusOK, gin.H{"keyword": keyword})
})
リクエスト
curl "http://localhost:8080/search?q=golang"
# {"keyword":"golang"}

値がないときにデフォルト値を与えたければ c.DefaultQuery を使います。ページネーションのパラメータによく使う方法です。

デフォルト値付きのクエリ
page := c.DefaultQuery("page", "1")
size := c.DefaultQuery("size", "20")

c.Query は値がなければ空文字列を返します。値の存在そのものを知る必要があれば、c.GetQuery を使えばよいです。

存在の確認
value, ok := c.GetQuery("q")
if !ok {
	// qパラメータ自体がない
}

同じキーが複数回来るときは c.QueryArray で配列を受け取ります。

配列クエリ
// /filter?tag=go&tag=web
tags := c.QueryArray("tag") // ["go", "web"]

ルーターグループ — 共通prefixをまとめる #

APIバージョンやリソースごとに、パスの前半部分が繰り返されます。ルーターグループで共通のprefixを一度だけ書きます。

ルーターグループ
r := gin.Default()

v1 := r.Group("/api/v1")
{
	v1.GET("/users", listUsers)
	v1.POST("/users", createUser)
	v1.GET("/users/:id", getUser)
}

上のコードは /api/v1/users のようなパスを作ります。グループを囲む波括弧は文法的に必須ではありませんが、どのルートがグループに属するかを視覚的にまとめてくれる慣習です。

グループはネストすることもできます。

ネストグループ
v1 := r.Group("/api/v1")
{
	users := v1.Group("/users")
	{
		users.GET("", listUsers)       // /api/v1/users
		users.GET("/:id", getUser)     // /api/v1/users/:id
	}
}

グループは単にパスをまとめるだけでは終わりません。グループ単位でミドルウェアを掛けられるので、たとえば認証が必要なルートだけを1つのグループに集める、といった形で活用します。この部分は #5 ミドルウェア で扱います。

ハンドラを関数に分離する #

ハンドラをルート登録部にインラインですべて書くと、main 関数がすぐに肥大化します。ハンドラは名前付きの関数に分離するほうがよいです。

ハンドラの分離
func getUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{"userID": id})
}

func main() {
	r := gin.Default()
	r.GET("/users/:id", getUser)
	r.Run()
}

ハンドラの型は gin.HandlerFunc、つまり func(c *gin.Context) です。この形さえ守れば、どこに定義してもルートに登録できます。ファイルを分ける本格的な構成は #7 プロジェクト構成 で整理します。

マッチしないパス — NoRoute #

登録されていないパスにリクエストが来ると、Ginはデフォルトの404を返します。レスポンス形式を自分で決めたければ、NoRoute ハンドラを登録します。

カスタム404
r.NoRoute(func(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{"error": "パスが見つかりません"})
})

API全体が一貫したJSONエラー形式を持つようにそろえるときに便利です。

まとめ #

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

  • メソッドごとに r.GETr.POSTr.PUTr.DELETE などでルートを登録
  • パスパラメータは :名前 で宣言し c.Param で読む、値は常に文字列
  • パスの残り全体はワイルドカード *名前 で受け取る
  • クエリストリングは c.Query、デフォルト値は c.DefaultQuery、配列は c.QueryArray
  • r.Group で共通prefixをまとめ、ネストとグループごとのミドルウェアが可能
  • ハンドラは func(c *gin.Context) 形式の名前付き関数に分離
  • マッチしないパスは NoRoute でレスポンス形式を制御

次の記事(#3 リクエストバインディングと検証)では、リクエストボディを構造体で受け取るバインディングと、validatorタグで入力を検証する方法を整理します。

X