Golang+GinでAPIを大量に書くことになりそうなので予習することにする。
コード自体はAI Agentで書こうと思うが、まずはGinのフィーチャーを把握する必要がある。
AI Agentを使用してAPI毎にフィーチャーを試せる学習用プロジェクトを構築する。
著者のスペックは、昔仕事でLaravelでWebアプリを書いたことがある。
[arst_toc tag=\"h4\"]
Ginについて
🚀 高速なパフォーマンス
martini に似たAPIを持ちながら、httprouter のおかげでそれより40倍以上も速いパフォーマンスがあります。
**基数木(Radix Tree)**ベースのルーティングを採用しており、メモリ効率が良く、高速なルーティングを実現しています。
他のGo製Webフレームワークと比較して、ベンチマークで優れた速度を示すことが多く、特に高スループットな REST API や マイクロサービス の構築に適しています。
Laravelは遅くて有名だったが、速いのは良いこと。
Golang自体ネイティブ実行だし、Golang用フレームワークの中でも速度にフィーチャーした構造。
たいした同時実行数を捌かないなら別に遅くても良いし、速いなら良いよね、ぐらい。
🧩 ミドルウェアのサポート
受信したHTTPリクエストを、ミドルウェアのチェーンと最終的なアクション(ハンドラー)で処理する仕組みを提供します。
ロガー、認証、GZIP圧縮など、様々な機能を簡単に組み込むことができます。
ミドルウェアくらい使えないと困るよね。認証を書きたい。
🛡️ クラッシュフリー
HTTPリクエスト処理中に発生したpanicをキャッチし、**リカバリー(回復)**する機能が組み込まれています。
これにより、サーバーがクラッシュするのを防ぎ、サービスを常に利用可能な状態に保ちます。
🔗 ルートのグループ化
認証が必要なルートやAPIのバージョンごとなど、関連するルートをグループ化して整理する機能があり、共通のミドルウェアを適用しやすいです。
フルスタックフレームワークではないので、これだけしか書かれていない。
シンプルであることは良いこと。
学習用プロジェクトの構成
いったん、こんな感じで構成。
  golang-gin/
  ├── docker-compose.yml
  ├── Dockerfile
  ├── go.mod
  ├── go.sum
  ├── main.go
  ├── README.md
  └── handlers/
      ├── hello.go          # Hello World API
      ├── params.go         # パラメータ処理
      ├── json.go           # JSON処理
      ├── middleware.go     # ミドルウェア
      ├── validation.go     # バリデーション
      ├── file.go           # ファイルアップロード
      └── grouping.go       # ルートグループ化
学習計画とAPI
API毎にフィーチャーを実装していくスタイルとする。
Claude Codeにその一覧を出力すると以下の通り。
  | No. | 機能           | エンドポイント              | メソッド | 説明                   |
  |-----|--------------|----------------------|------|----------------------|
  | 1   | 基本的なルーティング   | /hello               | GET  | Hello World を返す基本API |
  | 2   | パスパラメータ      | /users/:id           | GET  | URL パスからパラメータを取得     |
  | 3   | クエリパラメータ     | /search              | GET  | クエリ文字列からパラメータを取得     |
  | 4   | JSON レスポンス   | /api/user            | GET  | 構造体を JSON で返す        |
  | 5   | JSON リクエスト   | /api/user            | POST | JSON をバインドして処理       |
  | 6   | フォームデータ      | /form                | POST | フォームデータの受け取り         |
  | 7   | バリデーション      | /api/register        | POST | 入力データのバリデーション        |
  | 8   | ファイルアップロード   | /upload              | POST | 単一ファイルのアップロード        |
  | 9   | 複数ファイルアップロード | /upload/multiple     | POST | 複数ファイルのアップロード        |
  | 10  | ミドルウェア (ログ)  | /api/protected       | GET  | カスタムミドルウェアの実装        |
  | 11  | ルートグループ化     | /v1/users, /v2/users | GET  | API バージョニング          |
  | 12  | エラーハンドリング    | /error               | GET  | エラーレスポンスの処理          |
  | 13  | カスタムバリデーター   | /api/validate        | POST | カスタムバリデーションルール       |
  | 14  | リダイレクト       | /redirect            | GET  | リダイレクト処理             |
  | 15  | 静的ファイル配信     | /static/*            | GET  | 静的ファイルの提供            |
Hello World
まずは Hello World を返すAPIを作る。
main.goは以下の通り。./handlers 以下に実態を書いていく。
package main
import (
        \"github.com/gin-gonic/gin\"
        \"github.com/ikuty/golang-gin/handlers\"
)
func main() {
        // Ginエンジンの初期化
        r := gin.Default()
        // Hello World API
        r.GET(\"/hello\", handlers.HelloHandler)
        // サーバー起動
        r.Run(\":8080\")
}
./handlers/hello.go は以下の通り。
package handlers
import (
        \"net/http\"
        \"github.com/gin-gonic/gin\"
)
// HelloHandler は Hello World を返すハンドラー
func HelloHandler(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
                \"message\": \"Hello World\",
        })
}
試す。入門した。
$ curl http://localhost:8080/hello
{\"message\":\"Hello World\"}
パスパラメータ
URL内にプレースホルダを設定し、URLのプレースホルダと対応する値を変数で受けられる機能。
package main
import (
        \"github.com/gin-gonic/gin\"
        \"github.com/ikuty/golang-gin/handlers\"
)
func main() {
        // Ginエンジンの初期化
        r := gin.Default()
        // 1. 基本的なルーティング
        r.GET(\"/hello\", handlers.HelloHandler)
        // 2. パスパラメータ
        r.GET(\"/users/:id\", handlers.GetUserByIDHandler)
        // サーバー起動
        r.Run(\":8080\")
}
./handlers/params.goは以下。
Laravelと同じところに違和感。型はどこいった..? 
Ginでは、パスパラメータは常に文字列(string)として取得される。
URLから取得したパラメータを別の型(intやuintなど)として扱いたい場合は、
取得した文字列を明示的に型変換する必要がある。
package handlers
import (
        \"net/http\"
        \"github.com/gin-gonic/gin\"
)
// GetUserByIDHandler は URL パスパラメータからユーザーIDを取得するハンドラー
func GetUserByIDHandler(c *gin.Context) {
        // パスパラメータ :id を取得
        id := c.Param(\"id\")
        c.JSON(http.StatusOK, gin.H{
                \"user_id\": id,
                \"message\": \"User ID retrieved from path parameter\",
        })
}
実行。
# 数値IDのテスト
  $ curl http://localhost:8080/users/123
  {\"message\":\"User ID retrieved from path parameter\",\"user_id\":\"123\"}
  # 文字列IDのテスト
  $ curl http://localhost:8080/users/alice
  {\"message\":\"User ID retrieved from path parameter\",\"user_id\":\"alice\"}
クエリパラメータ
クエリパラメータを受け取る方法は以下。
まぁシンプル。
package handlers
import (
        \"net/http\"
        \"github.com/gin-gonic/gin\"
)
// SearchHandler は クエリパラメータから検索条件を取得するハンドラー
func SearchHandler(c *gin.Context) {
        // クエリパラメータを取得
        query := c.Query(\"q\")                   // ?q=keyword
        page := c.DefaultQuery(\"page\", \"1\")     // ?page=2 (デフォルト値: \"1\")
        limit := c.DefaultQuery(\"limit\", \"10\")  // ?limit=20 (デフォルト値: \"10\")
        // オプショナルなパラメータ
        sort := c.Query(\"sort\") // 値がない場合は空文字列
        c.JSON(http.StatusOK, gin.H{
                \"query\":   query,
                \"page\":    page,
                \"limit\":   limit,
                \"sort\":    sort,
                \"message\": \"Query parameters retrieved successfully\",
        })
}
実行結果は以下。
  # パスパラメータ
  $ curl http://localhost:8080/users/123
  {\"message\":\"User ID retrieved from path parameter\",\"user_id\":\"123\"}
  # クエリパラメータ
  $ curl \"http://localhost:8080/search?q=test&page=2\"
  {\"limit\":\"10\",\"message\":\"Query parameters retrieved successfully\",\"page\":\"2\",\"query\":\"test\",\"sort\":\"\"}
JSONリクエスト/JSONレスポンス
Content-Type: application/json で半構造化データ(JSON)を送り、構造体で受けることができる。
また、構造体を Content-Type: application/json でJSON文字列を返すことができる。
構造体のメンバに型を定義しておくことで、文字列がメンバ型に変換(バインド)できる。
まずルーティングは以下の通り。
package main
import (
        \"github.com/gin-gonic/gin\"
        \"github.com/ikuty/golang-gin/handlers\"
)
func main() {
        // Ginエンジンの初期化
        r := gin.Default()
        // 4. JSON レスポンス
        r.GET(\"/api/user\", handlers.GetUserHandler)
        // 5. JSON リクエスト
        r.POST(\"/api/user\", handlers.CreateUserHandler)
        // サーバー起動
        r.Run(\":8080\")
}
ハンドラは以下の通り。
バインドの記述が興味深い。バインド時にバリデーションを実行している。
package handlers
import (
        \"net/http\"
        \"github.com/gin-gonic/gin\"
)
// User 構造体
type User struct {
        ID       int    `json:\"id\"`
        Name     string `json:\"name\"`
        Email    string `json:\"email\"`
        Age      int    `json:\"age\"`
        IsActive bool   `json:\"is_active\"`
}
// GetUserHandler は 構造体を JSON で返すハンドラー
func GetUserHandler(c *gin.Context) {
        // サンプルユーザーデータ
        user := User{
                ID:       1,
                Name:     \"John Doe\",
                Email:    \"john@example.com\",
                Age:      30,
                IsActive: true,
        }
        c.JSON(http.StatusOK, user)
}
// CreateUserRequest はユーザー作成リクエストの構造体
type CreateUserRequest struct {
        Name  string `json:\"name\" binding:\"required\"`
        Email string `json:\"email\" binding:\"required,email\"`
        Age   int    `json:\"age\" binding:\"required,gte=0,lte=150\"`
}
// CreateUserHandler は JSON リクエストをバインドして処理するハンドラー
func CreateUserHandler(c *gin.Context) {
        var req CreateUserRequest
        // JSON をバインド(バリデーションも実行される)
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{
                        \"error\":   \"Invalid request\",
                        \"details\": err.Error(),
                })
                return
        }
        // 作成されたユーザーを返す(実際はDBに保存する)
        user := User{
                ID:       100, // 仮のID
                Name:     req.Name,
                Email:    req.Email,
                Age:      req.Age,
                IsActive: true,
        }
        c.JSON(http.StatusCreated, gin.H{
                \"message\": \"User created successfully\",
                \"user\":    user,
        })
}
実行結果は以下。
  1. GET - JSON レスポンス
  $ curl http://localhost:8080/api/user
  {\"id\":1,\"name\":\"John Doe\",\"email\":\"john@example.com\",\"age\":30,\"is_active\":true}
  2. POST - 正常なリクエスト
  $ curl -X POST http://localhost:8080/api/user 
    -H \"Content-Type: application/json\" 
    -d \'{\"name\":\"Alice\",\"email\":\"alice@example.com\",\"age\":25}\'
  {\"message\":\"User created successfully\",\"user\":{\"id\":100,\"name\":\"Alice\",\"email\":\"alice@example.com\",\"age\":25,\"is_active\":true}}
  3. POST - バリデーションエラー(メール形式)
  $ curl -X POST http://localhost:8080/api/user 
    -H \"Content-Type: application/json\" 
    -d \'{\"name\":\"Bob\",\"email\":\"invalid-email\",\"age\":30}\'
  {\"details\":\"Key: \'CreateUserRequest.Email\' Error:Field validation for \'Email\' failed on the \'email\' tag\",\"error\":\"Invalid request\"}
  4. POST - バリデーションエラー(年齢範囲)
  $ curl -X POST http://localhost:8080/api/user 
    -H \"Content-Type: application/json\" 
    -d \'{\"name\":\"Charlie\",\"email\":\"charlie@example.com\",\"age\":200}\'
  {\"details\":\"Key: \'CreateUserRequest.Age\' Error:Field validation for \'Age\' failed on the \'lte\' tag\",\"error\":\"Invalid request\"}
フォームデータ
フォームデータの送信例。ルーティングは以下。
POSTで送ったフィールドを丸っと構造体にする例と、
それぞれのフィールドを個別に取得する例の2つ。
package main
import (
        \"github.com/gin-gonic/gin\"
        \"github.com/ikuty/golang-gin/handlers\"
)
func main() {
        // Ginエンジンの初期化
        r := gin.Default()
        // 6. フォームデータ
        r.POST(\"/form/login\", handlers.LoginHandler)
        r.POST(\"/form/post\", handlers.PostFormHandler)
        // サーバー起動
        r.Run(\":8080\")
}
ハンドラは以下。丸っとフォームデータを構造体にバインドできるし、
個別にアクセスすることもできる。
シンプルというか、少ない道具でなんとかするタイプ。
package handlers
import (
        \"net/http\"
        \"github.com/gin-gonic/gin\"
)
// LoginForm はログインフォームの構造体
type LoginForm struct {
        Username string `form:\"username\" binding:\"required\"`
        Password string `form:\"password\" binding:\"required,min=6\"`
        Remember bool   `form:\"remember\"`
}
// LoginHandler はフォームデータを受け取るハンドラー
func LoginHandler(c *gin.Context) {
        var form LoginForm
        // フォームデータをバインド
        if err := c.ShouldBind(&form); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{
                        \"error\":   \"Invalid form data\",
                        \"details\": err.Error(),
                })
                return
        }
        // 実際はここで認証処理を行う
        c.JSON(http.StatusOK, gin.H{
                \"message\":  \"Login successful\",
                \"username\": form.Username,
                \"remember\": form.Remember,
        })
}
// PostFormHandler は個別にフォームフィールドを取得するハンドラー
func PostFormHandler(c *gin.Context) {
        // 個別のフォームフィールドを取得
        title := c.PostForm(\"title\")
        content := c.DefaultPostForm(\"content\", \"No content provided\")
        tags := c.PostFormArray(\"tags\") // 配列として取得
        c.JSON(http.StatusOK, gin.H{
                \"message\": \"Form data received\",
                \"title\":   title,
                \"content\": content,
                \"tags\":    tags,
        })
}
実行例は以下。
1. ログインフォーム - 正常
  $ curl -X POST http://localhost:8080/form/login -d \"username=john&password=secret123\"
  {\"message\":\"Login successful\",\"remember\":false,\"username\":\"john\"}
  2. ログインフォーム - remember 付き
  $ curl -X POST http://localhost:8080/form/login -d \"username=alice&password=pass123&remember=true\"
  {\"message\":\"Login successful\",\"remember\":true,\"username\":\"alice\"}
  3. ログインフォーム - バリデーションエラー
  $ curl -X POST http://localhost:8080/form/login -d \"username=bob&password=123\"
  {\"details\":\"Key: \'LoginForm.Password\' Error:Field validation for \'Password\' failed on the \'min\' tag\",\"error\":\"Invalid form data\"}
  4. 投稿フォーム - 配列データ
  $ curl -X POST http://localhost:8080/form/post -d \"title=Hello&content=World&tags=go&tags=gin&tags=api\"
  {\"content\":\"World\",\"message\":\"Form data received\",\"tags\":[\"go\",\"gin\",\"api\"],\"title\":\"Hello\"}
まとめ
いったん、以下を試した。
  基本的なルーティング
  バスパラメタ・クエリパラメタ
  JSON Request/Response
  フォームデータ
シンプルすぎてClaude Codeが機能を絞っているのか疑ったが、
公式を読む限り、若干バリエーションが増える程度の様子。
これならわざわざClaudeに入門コースを作ってもらわなくても上から読めば良いかな。