Gin基礎 #2 ルーティングとハンドラ
前回の記事で最初の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 ハンドラを登録します。
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "パスが見つかりません"})
})API全体が一貫したJSONエラー形式を持つようにそろえるときに便利です。
まとめ #
今回の記事で整理した内容:
- メソッドごとに
r.GET、r.POST、r.PUT、r.DELETEなどでルートを登録 - パスパラメータは
:名前で宣言しc.Paramで読む、値は常に文字列 - パスの残り全体はワイルドカード
*名前で受け取る - クエリストリングは
c.Query、デフォルト値はc.DefaultQuery、配列はc.QueryArray r.Groupで共通prefixをまとめ、ネストとグループごとのミドルウェアが可能- ハンドラは
func(c *gin.Context)形式の名前付き関数に分離 - マッチしないパスは
NoRouteでレスポンス形式を制御
次の記事(#3 リクエストバインディングと検証)では、リクエストボディを構造体で受け取るバインディングと、validatorタグで入力を検証する方法を整理します。