Gin入門
この章では、GoのWebフレームワークであるGinの基本を学び、ハンズオン形式で学習します。これにより、ルーティング・パスパラメータ・リクエストボディ・ミドルウェアなど、GinでWebサーバを作る基本が身につきます。
1. 本章の概要
1.1 本章の目的
前章までで、net/http を使ったWebサーバの作り方(net/http入門)と、GORMによるDB操作(GORM入門)を扱ってきました。標準ライブラリだけでもWebサーバは十分に作れますが、ハンドラごとのJSONレスポンス書き出しやリクエストボディの解析、ミドルウェアの用意など、繰り返し書く定型部分が増えてくると記述量が膨らみます。Gin はこの定型部分を薄くラップし、ハンドラ・レスポンス・バリデーションを短く書けるようにしたGoのWebフレームワークです。次章の GoでREST APIを作ろう でGin+GORMの組み合わせを扱う前段として、Gin単体の書き味を押さえます。
1.2 ハンズオンの流れ
Ginで最小のWebサーバを立ち上げるところから始めて、パスパラメータ・クエリパラメータ・JSONリクエストボディ・自動バリデーション・ミドルウェア・ルータグループまで段階的に実装します。要所で net/http の書き方にも触れながら、Ginの書き味を確認します。
1.3 事前準備
前提となる講座
この章では、以下の知識を前提としています。自信がない場合は先に関連講座を実施してみましょう。
| 講座名 | 必要な知識 |
|---|---|
| Webアプリケーションの基本 | HTTPリクエスト・レスポンスやWebアプリケーションの基本構造 |
必要なツール
この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。
| ツール名 | 関連箇所 | 理由 |
|---|---|---|
| Visual Studio Code | Visual Studio Codeのインストール | Ginを使ったコードを記述するエディタとして使用する |
| Go | Goのインストール | Ginを利用するコードのランタイムとして使用する |
2. Ginとは
Gin は、Goで最も広く使われているWebフレームワークです。net/http の薄いラッパとして設計されており、ルーティング・JSONバインド・ミドルウェアなどWebサーバでよく書く処理を短く記述できます。Ginには以下のような特徴があります。
- 高速なルータ(Radix Tree)を採用しており、多数のルートでも安定した性能が出る
c.JSON/c.Param/c.Queryなどの簡潔なAPIで、リクエストの取り出しとレスポンス書き出しをまとめて書ける- 構造体タグ
binding:"required"によるバリデーションがc.ShouldBindJSONに組み込まれている - ミドルウェアがチェーン可能で、ログの出力や認証などの共通処理を差し込める
- パニックからの自動リカバリが
gin.Default()に含まれている
| 📝 GoのWebフレームワークの位置づけ |
|---|
Go のWebフレームワークは複数存在します。代表的なものとして Gin・Echo・Fiber などがあり、いずれも net/http を土台に「よく書く処理を短く書けるようにする」方向のライブラリです。Gin は利用者数が多くドキュメント・サンプルが豊富で、Go でWebフレームワークを最初に触るのに扱いやすい選択肢です。 |
3. 最小のWebサーバ
Ginで最小のWebサーバを立ち上げるまでの流れを確認します。
3.1 プロジェクトの準備
任意の場所に hello-gin フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した hello-gin フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。
hello-gin/ ← このフォルダを作成
以下のコマンドで、Go がインストールされていることを確認します。
go version
以下のように Go のバージョンが表示されれば、インストールは確認できています。
go version go1.x.x darwin/arm64
バージョンが表示されない場合は、Goのインストール を先に実施してください。
インストールが確認できたら、モジュールを初期化するために、go mod init コマンドを実行します。
go mod init hello-gin
以下のような実行結果が表示されます。
go: creating new go.mod: module hello-gin
続いて、Gin本体を go get コマンドで取得します。
go get github.com/gin-gonic/gin
以下のような実行結果が表示されます(バージョンは環境により変わります)。
go: added github.com/gin-gonic/gin v1.10.0
go.mod に依存関係が追記され、go.sum が生成されていれば準備完了です。
3.2 main.go の作成
Ginでは、gin.Engine 型のインスタンスを作成し、HTTPメソッドとパスに対応するハンドラを登録します。書き方は以下のとおりです。
r := gin.Default() // ロガー・リカバリ入りの Engine を作成
r.GET("パス", func(c *gin.Context) {
c.JSON(ステータスコード, gin.H{"キー": "値"}) // JSONレスポンスを返す
})
r.Run(":ポート") // 指定ポートでサーバを起動
gin.Default() はロガーとパニックリカバリ用のミドルウェアが組み込まれた gin.Engine を返します。ハンドラは *gin.Context を受け取り、c.JSON などの短いAPIでレスポンスを返します。
Visual Studio Codeのエクスプローラーで hello-gin フォルダを右クリックし、「新しいファイル」を選択して main.go を作成します。作成したファイルに以下の内容を記述して保存します。これは、ポート 8080 で待ち受け、ルートパス(/)へのGETリクエストに {"message": "Hello, Gin!"} を返す最小のWebサーバです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin!"})
})
r.Run(":8080")
}
以下のコマンドでサーバを起動します。
go run main.go
以下のような実行結果が表示されます。
[GIN-debug] Listening and serving HTTP on :8080
Gin独自のデバッグログとともに、ポート 8080 でサーバがリクエスト待ちの状態になりました。
別のターミナルから curl でアクセスします。
curl http://localhost:8080/
以下のような実行結果が表示されます。
{"message":"Hello, Gin!"}
登録したハンドラが返した gin.H{"message": "Hello, Gin!"} が、そのままJSONレスポンスとして curl の出力に現れました。数行のコードで、JSONを返すWebサーバが立ち上がっていることが確認できます。
コードを解説します。
r := gin.Default()
gin.Default() はロガーとパニックリカバリ用のミドルウェアが組み込まれた gin.Engine を返します。以降のルート登録はすべてこの r に対して行います。
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin!"})
})
r.GET("パス", ハンドラ) で「このパスへのGETリクエストを、このハンドラで処理する」と登録します。ハンドラは *gin.Context の1引数のみを受け取ります。net/http の (w, r) の2引数を1つに束ねたのが gin.Context で、レスポンス書き出しとリクエスト参照の両方をこのオブジェクト経由で行います。
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin!"})
c.JSON はステータスコードと任意のGoの値を渡すと、Content-Type: application/json の設定・ステータスコードの書き込み・JSONエンコードをまとめて行います。gin.H は map[string]any の別名で、JSONオブジェクトを短く書くためのショートカットです。
r.Run(":8080")
r.Run は指定ポートでサーバを起動する関数で、内部では http.ListenAndServe(":8080", r) 相当の処理を行います。
| 📝 gin.Default と gin.New の違い |
|---|
gin.Default() はロガーとパニックリカバリ用のミドルウェアが最初から組み込まれた gin.Engine を返します。何のミドルウェアも持たない空の gin.Engine がほしい場合は gin.New() を使い、必要なミドルウェアを自分で Use する形になります。学習・開発時は gin.Default() を使うのが扱いやすいです。 |
4. ルーティングとリクエスト処理
ここからは、各節ごとに main.go を書き換えながら、Ginの主要なリクエスト処理を扱います。
4.1 パスパラメータ
パスパラメータは、URLのパスの一部をパラメータとして受け取る仕組みです。例えば /users/123 というURLの 123 の部分(ユーザID)をハンドラで受け取りたい場合、URLに埋め込まれた 123 をパラメータとして扱えるようにします。
Ginでは、パスの中に :パラメータ名 を含めることでパスパラメータを宣言し、c.Param("パラメータ名") で値を取り出します。書き方は以下のとおりです。
r.GET("/パス/:パラメータ名", func(c *gin.Context) {
値 := c.Param("パラメータ名") // パスパラメータの値を取得
})
main.go を以下の内容で置き換えて保存します。これは、/hello/:name にアクセスすると、パス内の :name を取り出して挨拶メッセージを返すコードです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello/:name", func(c *gin.Context) {
name := c.Param("name")
c.JSON(http.StatusOK, gin.H{"message": "こんにちは、" + name + "さん!"})
})
r.Run(":8080")
}
| 💡 ポイント |
|---|
Ginで立ち上げたWebサーバは --reload のような自動再起動機能を標準では持たないため、コードを変更したら Ctrl+C でサーバを停止してから go run main.go で再起動する必要があります。以降の節でも同じ手順で再起動します。 |
既存のサーバは Ctrl+C で停止し、以下のコマンドで再起動します。
go run main.go
別のターミナルで以下のコマンドを実行します。
curl http://localhost:8080/hello/Taro
以下のような実行結果が表示されます。
{"message":"こんにちは、Taroさん!"}
URLの Taro の部分がパスパラメータとして関数に渡され、{"message": "こんにちは、Taroさん!"} としてレスポンスに反映されています。
コードを解説します。
r.GET("/hello/:name", func(c *gin.Context) {
name := c.Param("name")
...
})
パスパターン /hello/:name の :name がパスパラメータの宣言です。c.Param("name") で対応する値を文字列として取り出します。net/http の Go 1.22 では /hello/{name} と書き r.PathValue("name") で取り出していましたが、Gin では : プレフィックスと c.Param を使います。
4.2 クエリパラメータ
クエリパラメータは、URLの ? 以降に キー=値 の形式で指定するパラメータです。例えば /search?q=cat&limit=5 というURLでは q=cat と limit=5 の2つがクエリパラメータで、検索条件やオプションのように可変のデータを渡すのによく使われます。
Ginでは、指定したキーの値を取得するには c.Query を使います。書き方は以下のとおりです(キーが存在しない場合は空文字列が返ります)。
値 := c.Query("キー名")
キーが未指定のときにデフォルト値を返したい場合は c.DefaultQuery を使います。
値 := c.DefaultQuery("キー名", "デフォルト値")
main.go を以下の内容で置き換えて保存します。これは、/search にアクセスすると、クエリパラメータ q(未指定時は空文字列)と limit(未指定時はデフォルト値 10)を取り出してJSONで返すコードです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/search", func(c *gin.Context) {
q := c.Query("q")
limit := c.DefaultQuery("limit", "10")
c.JSON(http.StatusOK, gin.H{"query": q, "limit": limit})
})
r.Run(":8080")
}
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run main.go
別のターミナルで、両方のパラメータを指定して確認します。
curl "http://localhost:8080/search?q=cat&limit=5"
以下のような実行結果が表示されます。
{"limit":"5","query":"cat"}
limit を省略した場合は DefaultQuery に指定した 10 が使用されます。
curl "http://localhost:8080/search?q=cat"
以下のような実行結果が表示されます。
{"limit":"10","query":"cat"}
コードを解説します。
q := c.Query("q")
limit := c.DefaultQuery("limit", "10")
c.Query("q") は URL のクエリ文字列から q の値を取り出します。キーが存在しない場合は空文字列を返します。c.DefaultQuery("limit", "10") は同様にクエリを取り出しつつ、未指定時に "10" を返す動きです。
| 📝 パスパラメータとクエリパラメータの違い |
|---|
パスパラメータはURLのパスの一部(/hello/Taro)、クエリパラメータはURLの ? 以降(/search?q=cat)で値を指定します。リソースを特定する場合はパスパラメータ、検索条件やオプションを指定する場合はクエリパラメータが使われるのが一般的です。 |
4.3 リクエストボディとバリデーション
POST や PUT などでJSONボディを受け取る場合、Ginでは構造体と c.ShouldBindJSON を組み合わせて扱います。書き方は以下のとおりです。
type 構造体名 struct {
フィールド名 型 `json:"キー名" binding:"required"` // binding:"required" で必須指定
}
var input 構造体名
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.ShouldBindJSON はリクエストボディのJSONを構造体にデコードし、同時に binding タグに基づくバリデーションを実行します。バリデーションに失敗した場合はエラーが返るため、400 Bad Request を返して処理を打ち切るのが一般的です。
main.go を以下の内容で置き換えて保存します。これは、/items にPOSTで送られたJSON({"name":"...", "price":...} 形式)を Item 構造体にデコードし、binding:"required" によるバリデーションを通したうえで受け取った内容をJSONで返すコードです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Item struct {
Name string `json:"name" binding:"required"`
Price float64 `json:"price" binding:"required"`
}
func main() {
r := gin.Default()
r.POST("/items", func(c *gin.Context) {
var input Item
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "アイテムを作成しました",
"item": input,
})
})
r.Run(":8080")
}
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run main.go
別のターミナルで、-d オプションでJSONボディをPOSTします。
curl -X POST http://localhost:8080/items -H "Content-Type: application/json" -d '{"name":"ノート","price":250}'
以下のような実行結果が表示されます。
{"item":{"name":"ノート","price":250},"message":"アイテムを作成しました"}
送信したJSONの name と price が Item 構造体にデコードされ、そのままレスポンスに反映されています。
必須フィールドの name を省略した場合の挙動も確認します。
curl -X POST http://localhost:8080/items -H "Content-Type: application/json" -d '{"price":250}'
以下のような実行結果が表示されます。
{"error":"Key: 'Item.Name' Error:Field validation for 'Name' failed on the 'required' tag"}
binding:"required" に基づくバリデーションが自動的に行われ、Name フィールドが必須であることを示すエラーが返されました。
コードを解説します。
type Item struct {
Name string `json:"name" binding:"required"`
Price float64 `json:"price" binding:"required"`
}
json:"name" のタグでJSONキーとGoのフィールドを対応付け、binding:"required" で必須フィールドを宣言します。binding タグには required 以外にも email(メールアドレス形式)や min・max(数値の範囲)など、多数のバリデーションルールがあります(詳しくは go-playground/validator に記載があります)。
var input Item
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.ShouldBindJSON(&input) はリクエストボディのJSONを input にデコードし、binding タグに基づくバリデーションを同時に実行します。デコード失敗・バリデーション失敗のいずれもエラーとして返るため、400 Bad Request を返して早期リターンする形が定型パターンです。
c.JSON(http.StatusCreated, gin.H{
"message": "アイテムを作成しました",
"item": input,
})
http.StatusCreated(201)を返しています。リソース作成が成功した場合の慣例的なステータスコードで、net/http の w.WriteHeader(http.StatusCreated) に相当します。
| 💡 ポイント |
|---|
c.ShouldBindJSON と似た関数に c.BindJSON がありますが、BindJSON はバリデーション失敗時に自動で 400 を返してしまうため、独自のエラー整形をしたい場面では扱いにくくなります。エラーレスポンスの形式を自分で決めたい場合は ShouldBindJSON を使い、err に応じてレスポンスを作る書き方が一般的です。 |
5. ミドルウェアとルータグループ
ログの出力や認証など、複数のハンドラで共通に行いたい処理は、ハンドラ本体に書くと重複が増えます。Ginではミドルウェアとして共通処理を切り出し、r.Use や Group で適用範囲を指定できます。
5.1 ミドルウェアの実装
ミドルウェアは gin.HandlerFunc を返す関数として書き、c.Next() を呼ぶことで次のハンドラに処理を渡します。書き方は以下のとおりです。
func ミドルウェア名() gin.HandlerFunc {
return func(c *gin.Context) {
// 前処理
c.Next() // 次のハンドラを呼ぶ
// 後処理
}
}
r.Use(ミドルウェア名()) // 全ルートに適用
c.Next() の前が「ハンドラ実行前に行いたい処理」、後が「ハンドラ実行後に行いたい処理」です。
main.go を以下の内容で置き換えて保存します。これは、リクエストの処理時間をログに出力する LoggerMiddleware を r.Use で全ルートに適用したうえで、動作確認用に /hello/:name エンドポイントを1つ登録するコードです。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
log.Printf("%s %s (%s)", c.Request.Method, c.Request.URL.Path, time.Since(start))
}
}
func main() {
r := gin.Default()
r.Use(LoggerMiddleware())
r.GET("/hello/:name", func(c *gin.Context) {
name := c.Param("name")
c.JSON(http.StatusOK, gin.H{"message": "こんにちは、" + name + "さん!"})
})
r.Run(":8080")
}
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run main.go
別のターミナルで以下のコマンドを実行します。
curl http://localhost:8080/hello/Taro
サーバを起動しているターミナル側に、以下のようなログが出力されます(gin.Default() の既定ログに続いて、自作のログが追記されます)。
2026/07/29 12:00:00 GET /hello/Taro (123.456µs)
LoggerMiddleware がメソッド(GET)・パス(/hello/Taro)・処理時間をまとめてログに書き出しています。
コードを解説します。
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
log.Printf("%s %s (%s)", c.Request.Method, c.Request.URL.Path, time.Since(start))
}
}
gin.HandlerFunc を返す関数として定義します。前処理として start := time.Now() で開始時刻を記録し、c.Next() で次のハンドラを呼び出し、戻ってきたところで time.Since(start) により所要時間を算出しています。
r.Use(LoggerMiddleware())
r.Use に渡したミドルウェアは、以降に登録される全ルートに適用されます。特定のルート群にだけ適用したい場合は、次に扱う Group に対して Use を呼びます。
5.2 ルータグループ
REST API を作っていると、/api/v1/users や /api/v1/products のように、複数のエンドポイントに同じURLプレフィックス(この場合 /api/v1)を持たせたい場面がよく出てきます。認証が必要な /admin/* 配下だけに共通のミドルウェアを適用したいケースも同様です。これを1つひとつのハンドラに書くと、同じプレフィックスやミドルウェアを何度も繰り返すことになります。
Ginでは、この問題を ルータグループ で解決できます。ルータグループは、複数のエンドポイントを1つにまとめて、共通のURLプレフィックスやミドルウェアを一括で適用できる仕組みです。書き方は以下のとおりです。
グループ変数 := r.Group("/共通プレフィックス")
{
グループ変数.GET("/パス", ハンドラ) // 実際のパスは "/共通プレフィックス/パス"
グループ変数.POST("/パス", ハンドラ)
}
r.Group("/共通プレフィックス") は、そのプレフィックスが自動で先頭に付く新しいルーティング先を返します。{ } は Go の構文上のブロックで、グループに属するルートを視覚的にまとめるための慣例的な書き方です(意味的にはなくても動作します)。
main.go を以下の内容で置き換えて保存します。これは、/api/v1 プレフィックスのルータグループを作り、その配下に /ping と /version の2つのエンドポイントを登録するコードです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
v1 := r.Group("/api/v1")
{
v1.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
v1.GET("/version", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "1.0.0"})
})
}
r.Run(":8080")
}
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run main.go
別のターミナルで以下のコマンドを実行します。
curl http://localhost:8080/api/v1/ping
以下のような実行結果が表示されます。
{"message":"pong"}
v1.GET("/ping", ...) として登録したハンドラが、実際には /api/v1/ping で公開されていることが確認できます。
curl http://localhost:8080/api/v1/version
以下のような実行結果が表示されます。
{"version":"1.0.0"}
同じグループに属する /api/v1/version にも /api/v1 のプレフィックスが自動で付いていることが分かります。
コードを解説します。
v1 := r.Group("/api/v1")
r.Group("/api/v1") を呼ぶと、/api/v1 を先頭に付けたルーティング先が返ります。以降 v1.GET("/ping", ...) のように登録したパスは、実際には /api/v1/ping として公開されます。
{
v1.GET("/ping", ...)
v1.GET("/version", ...)
}
グループに属するルートを { } のブロックでくくるのは、Goの構文上のブロックであり慣例的な書き方です。処理上の意味はありませんが、グループの範囲が視覚的にまとまるため、REST APIのバージョン分割やモジュール分割で読みやすくなります。
| 💡 ポイント |
|---|
ルータグループは共通ミドルウェアの適用にも使えます。admin := r.Group("/admin", 認証ミドルウェア) のようにグループ作成時に渡すか、admin.Use(認証ミドルウェア) で後付けすることで、そのグループ配下のルートにだけミドルウェアが適用されます。認証が必要なエンドポイントと不要なエンドポイントを分けたい場面で使えます。 |
6. まとめ
この章では、Ginの基本を学びつつ、実際にコードでルーティング・リクエスト処理・ミドルウェアの実装を体験しました。
- Ginは、
net/httpを薄くラップしたGoのWebフレームワークで、Goで最も広く使われている gin.Default()でロガー・リカバリ入りのgin.Engineを作成し、r.Run(":ポート")でサーバを起動できるr.GET/r.POSTなどでHTTPメソッドとパスを1行で登録でき、ハンドラは*gin.Contextの1引数で書けるc.JSON(status, gin.H{...})により、Content-Type設定・ステータスコード書き込み・JSONエンコードを1行で行える- パスパラメータは
:name+c.Param("name")、クエリパラメータはc.Query/c.DefaultQueryで取り出せる - リクエストボディは
c.ShouldBindJSON(&input)でデコードとバリデーション(binding:"required"など)を同時に扱える - ミドルウェアは
gin.HandlerFuncを返す関数として書き、r.Useで全ルートに適用できる r.Group("/api/v1")によるルータグループで、共通プレフィックスや共通ミドルウェアを持つエンドポイントをまとめて扱える
次の章では、GoでREST APIを作る流れをハンズオン形式で体験します。