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