Golang + Gin Framework で Hello World してみた話 〜基本的なルーティング、バスパラメタ・クエリパラメタ・JSON Req/Res、フォームデータ

Golang+GinでAPIを大量に書くことになりそうなので予習することにする。
コード自体はAI Agentで書こうと思うが、まずはGinのフィーチャーを把握する必要がある。
AI Agentを使用してAPI毎にフィーチャーを試せる学習用プロジェクトを構築する。

著者のスペックは、昔仕事でLaravelでWebアプリを書いたことがある。

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に入門コースを作ってもらわなくても上から読めば良いかな。