Go

Gin(Go)とは?v1.12.0の最小構成・本番設定と標準net/httpとの使い分け

Gin は Go で書かれた HTTP ルーター兼 Web フレームワークです。ORM もテンプレートエンジンの強制もなく、担当するのは「経路解決」「ミドルウェア連結」「リクエストのバインドとレスポンス描画」の3つに絞られています。ところが「go gin」で上位に出る日本語記事を実際に読むと、入門コードで終わっていて、本番へ出す直前に効く release mode や信頼プロキシの設定、そもそも Gin を入れるべきかの判断材料までは踏み込んでいません。この記事では v1.12.0 を実際にビルドして動かし、最小構成から本番前に変える設定、そして Go 1.22 以降の標準 net/http で足りる条件までを実測値付きで整理します。

まとめ

Gin v1.12.0(2026年2月28日リリース)は Go 1.25 以上を要求します。公式リリースアナウンスが「最小サポート Go バージョンは 1.25 になった」と明記しており、v1.11.0(go.mod は go 1.23.0)から上げる際の最初の関門です。導入自体は go get github.com/gin-gonic/[email protected] の1コマンド(新規プロジェクトなら go mod init を含めて3コマンド)で済み、import を含めて12行のファイル1枚で JSON API が動きます。

本番へ出す前に変えるべき設定は2つだけです。GIN_MODE=release によるモード切り替えと、SetTrustedProxies による信頼プロキシの限定。既定のままだと起動ログに「すべてのプロキシを信頼している。安全ではない」という警告が出続けます。

性能面では、Gin のルーティングは手元の計測で1リクエストあたり約220ナノ秒・ヒープ確保ゼロ、標準の http.ServeMux は約930ナノ秒・2回のヒープ確保でした。約4.2〜4.3倍の差ですが、絶対値は0.7マイクロ秒です。DB アクセスやネットワーク I/O が支配的な実運用では、この差を選定理由にはできません。一方 JSON エンコーダを -tags=sonic で差し替えると、100要素の配列描画が約10.3万ナノ秒から約4.0万ナノ秒へ落ちました。応答が大きい API ほど効きます。

結論として、パスパラメータ・ルートグループ・バインドとバリデーションをまとめて使うなら Gin を入れる価値があります。逆に、エンドポイントが十数本でバリデーションも自前で書いている規模なら、Go 1.22 以降の標準 ServeMux で十分です。以下、各判断の根拠を順に示します。

Ginの正体と「Martini比40倍」の出どころ

「ジン」と読むGo製HTTPフレームワークの守備範囲

Gin は「ジン」と読みます。蒸留酒の gin と同じ綴りで、公式サイトのロゴもレモンを挿したグラスをかたどったものです。Go 由来の造語ではないため読み方に迷いやすく、日本語の技術文書では「ギン」と誤記されることもあります。

守備範囲は狭く、そこが利点です。GitHub リポジトリ(star 89,086、MIT ライセンス、最終更新 2026年8月4日時点)が提供するのは、URL とハンドラの対応付け、ミドルウェアの連結、リクエストボディの構造体への流し込み、そして JSON・XML・HTML などの描画までです。データベース接続は含まれません。Rails や Django のようなフルスタック構成を期待して導入すると、認証もマイグレーションも自前で組むことになります。

40倍という数字が測っているものと2026年時点での有効性

「Martini より最大40倍速い」という記述は Gin 公式 README の冒頭にあり、多くの日本語記事がそのまま引用しています。ただし2点、押さえておく必要があります。

第一に、これは Gin 自身が公開している自己申告値で、独立した第三者ベンチマークではありません。README のベンチマーク表(GitHub API のルート定義を使った経路解決の計測)では、Gin が 27,364 ns/op・0 allocs、Echo が 38,479 ns/op・0 allocs、chi が 238,331 ns/op・87,696 B/op と並びます。測っているのは経路解決であって、アプリケーション全体のスループットではありません。加えてこの表には計測日も各フレームワークの版も注記がなく、同じ表に Martini や Tiger Tonic といった現在は動いていないプロジェクトが並んでいます。掲載値をそのまま2026年の Echo や chi の実力として読まないでください。

第二に、比較対象の Martini は事実上終わっています。github.com/go-martini/martini は GitHub API の pushed_at が2022年3月29日ですが、この値は全ブランチを横断した最終push時刻です。既定ブランチの最終コミットを commits API で見ると2017年1月21日で、本体は9年以上動いていません。2026年に Go の Web フレームワークを選ぶとき、Martini は選択肢に入りません。つまりこの40倍という数字は、Gin が登場した当時の立ち位置を説明するものであって、現在の選定判断には使えないということです。今 Gin と比べるべき相手は Echo、chi、Fiber、そして標準ライブラリの net/http です(本記事の後半、「Echo・chi・Fiberとの選定基準」以降で扱います)。

v1.12.0の導入で最初につまずくGo 1.25要求

go getとバージョン確認、go.modが書き換わる条件

導入は3コマンドです。

go mod init example/api
go get github.com/gin-gonic/[email protected]
go list -m github.com/gin-gonic/gin
# => github.com/gin-gonic/gin v1.12.0

ここで踏みやすいのが Go のバージョン要求です。v1.12.0 の公式リリースアナウンスは「最小サポート Go バージョンは 1.25 になった」と記載しています。実物も同じで、v1.12.0 タグの go.modgo 1.25.0 を宣言済みです(v1.11.0 は go 1.23.0)。紛らわしいのは GitHub のリリースページに並ぶコミット一覧で、そこには「bump minimum Go version to 1.24 and update workflows」という行が残っています。これは開発途中で一度通過した値です。同じ一覧の後ろに「ci: update Go version support to 1.25+ across CI and docs」があり、CHANGELOG に載っているのも後者だけです。最終的な要求は 1.25 で確定しています。コミット一覧の1行だけを見て 1.24 で通ると判断すると、ビルド段階で止まります。

自分のモジュールの go ディレクティブが 1.24 以下のまま go build を実行すると、次のように拒否されます。

$ go build ./...
go: updates to go.mod needed; to update it:
	go mod tidy

指示どおり go mod tidy を走らせると、エラーは消える代わりに go.modgo ディレクティブが黙って 1.25.0 へ引き上げられます。CI で古い Go を固定している環境では、この書き換えがそのまま CI の失敗になります。Gin を上げる前に、ビルド環境の Go が 1.25 以上かを確認してください。Go 側のバージョン確認と移行の手順はGo 1.27の新機能・変更点まとめ|最新バージョンの確認方法と移行ガイドにまとめています。

ファイル1枚の最小サーバーと起動ログの読み方

動く最小構成は、import を含めて実質12行です。

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "pong"})
	})
	r.Run(":8080")
}

go run . で起動し curl http://127.0.0.1:8080/ping を叩くと {"message":"pong"} が返ります。このとき標準出力に出る内容が、そのまま次章以降の伏線になります。

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2026/08/11 - 01:04:41 | 200 | 66.216µs |       127.0.0.1 | GET      "/ping"

この出力から読み取れるのは、組み込み済みミドルウェア・既定モード・プロキシ信頼範囲の3点です。gin.Default() はロガーと Recovery を既に組み込んでいます(実際に Handlers の本数を数えると gin.Default() が2本、gin.New() が0本です)。既定は debug モードで、本番では release へ切り替えろと自ら警告しています。そして、すべてのプロキシを信頼する設定が安全でないと明示されています。後ろの2つは「本番投入前に必ず変える2つの設定」で潰します。

ルーティングの記述と基数木による経路解決

パスパラメータ・ワイルドカードの受け取り方

Gin のルーターは httprouter 由来の基数木(radix tree)で、登録済みの経路を共通接頭辞ごとにまとめて保持します。ルート数が増えても線形に走査しないため、経路解決の時間がルート数にほとんど比例しません。書き方は次の2種類です。なお以降のコードは、前節の最小サーバーの続き(rgin.Default() の戻り値、net/httpfmt は import 済み)として読んでください。

r.GET("/users/:id", func(c *gin.Context) {
	c.JSON(200, gin.H{
		"id": c.Param("id"),               // :id にマッチした値
		"q":  c.DefaultQuery("q", "none"), // ?q= が無ければ "none"
	})
})

r.GET("/files/*filepath", func(c *gin.Context) {
	c.String(200, "path=%s", c.Param("filepath"))
})

:id はスラッシュで区切られた1階層だけにマッチし、*filepath は残り全体を飲み込みます。手元で GET /users/42?q=x を投げると {"id":"42","q":"x"}GET /files/a/b/c.txt では path=/a/b/c.txt が返りました。ワイルドカードの値は先頭のスラッシュを含む点に注意してください。ファイルパスとして連結する前に filepath.Base などで正規化しないと、ディレクトリトラバーサルの入口になります。

登録していない経路は 404 page not found という平文が返ります。JSON API として一貫させたい場合は r.NoRoute で自前のハンドラを登録してください。

グループ化と静的ファイル配信の書き分け

API のバージョニングや認証の適用範囲を揃えるときは Group を使います。グループに Use したミドルウェアは、そのグループ配下だけに適用されます。

api := r.Group("/api/v1")
api.Use(authRequired()) // このグループ配下だけ認証
{
	api.POST("/users", createUserHandler)
	api.GET("/users/:id", getUserHandler)
}

r.Static("/assets", "./public") // ディレクトリを丸ごと公開
r.StaticFile("/favicon.ico", "./favicon.ico")

静的ファイルは Static(ディレクトリ)、StaticFile(単一ファイル)、StaticFShttp.FileSystem 実装を渡す)の3つが用意されています。embed パッケージで実行ファイルに埋め込んだアセットを配る場合は StaticFS を使います。ただし、実運用で画像や JS を大量に配るなら、フレームワークの前段に CDN か nginx を置くほうが確実です。Gin の静的配信は管理画面のアセット程度に留めるのが妥当な線引きです。

ミドルウェアの連結と自作ハンドラの書き方

前節で api.Use(authRequired()) と書いた authRequired() のような自作ミドルウェアは、gin.HandlerFunc を返す関数として定義します。c.Next() を呼ぶと後続のハンドラへ進み、戻ってきた後の行が後処理になります。c.Set で入れた値は同じリクエストの後続ハンドラから c.GetString などで取り出せます。

func RequestID() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.GetHeader("X-Request-Id")
		if id == "" {
			id = "generated" // 実際は UUID 等を採番する
		}
		c.Set("requestID", id)
		c.Header("X-Request-Id", id)
		c.Next() // これ以降が後続ハンドラの処理後に走る
	}
}

r := gin.New()
r.Use(RequestID())
r.GET("/who", func(c *gin.Context) {
	c.JSON(200, gin.H{"requestID": c.GetString("requestID")})
})

手元で X-Request-Id: abc-123 を付けて GET /who を叩くと {"requestID":"abc-123"} が返り、ヘッダを付けずに叩くと {"requestID":"generated"} が返りました。いずれもレスポンスヘッダに X-Request-Id が乗ります。処理を打ち切りたい場合は c.Next() の代わりに c.AbortWithStatusJSON(401, ...) を呼びます。return するだけでは後続ハンドラが実行される点に注意してください。

リクエストのバインドとバリデーションエラーの実際の形

ShouldBindJSONが返すエラー文字列とAPI設計への影響

Gin は構造体タグでバリデーションを宣言できます。検証は go-playground/validator が担当します。

type createUser struct {
	Name  string `json:"name"  binding:"required"`
	Email string `json:"email" binding:"required,email"`
	Age   int    `json:"age"   binding:"gte=0,lte=130"`
}

api.POST("/users", func(c *gin.Context) {
	var in createUser
	if err := c.ShouldBindJSON(&in); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusCreated, in)
})

ここで見落とされがちなのが、エラーをそのまま返したときの中身です。{"name":"taro","email":"bad","age":200} を投げると、400 とともに次の文字列が返りました。

{"error":"Key: 'createUser.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'createUser.Age' Error:Field validation for 'Age' failed on the 'lte' tag"}

Go の構造体名がそのままクライアントに漏れ、改行入りの1本の文字列にすべての違反が連結されています。これを公開 API のレスポンスにしてはいけません。内部の型名を露出させるうえ、フロントエンドがフィールド単位でエラーを表示できないためです。

詰め直しは十数行で済みます。validator.ValidationErrors の実体は []FieldError なので、Field()Tag() はスライスではなく各要素のメソッドです。スライスに直接生やそうとするとコンパイルが通りません。

type fieldError struct {
	Field string `json:"field"`
	Rule  string `json:"rule"`
}

func validationErrors(err error) []fieldError {
	var ve validator.ValidationErrors
	if !errors.As(err, &ve) {
		return nil
	}
	out := make([]fieldError, 0, len(ve))
	for _, fe := range ve { // ValidationErrors は []FieldError
		out = append(out, fieldError{Field: fe.Field(), Rule: fe.Tag()})
	}
	return out
}

ハンドラ側で c.JSON(http.StatusBadRequest, gin.H{"errors": validationErrors(err)}) と返すと、同じ入力に対する応答が {"errors":[{"field":"Email","rule":"email"},{"field":"Age","rule":"lte"}]} になります。フィールド名と違反した規則が分離されるので、フロント側は入力欄ごとにメッセージを出せます。この一手間を省いた API は、後からフロント側の実装で必ず揉めます。

クエリ・フォーム・URIそれぞれのバインド関数

入力元ごとに関数が分かれています。ShouldBindJSON はリクエストボディの JSON、ShouldBindQuery はクエリ文字列、ShouldBindUri はパスパラメータを対象に、いずれも同じ binding タグで検証されます。

単発の値を取るだけなら c.Param("id")c.Query("q")c.PostForm("name") で足ります。使い分けの基準は単純で、検証したい項目が2つ以上あるなら構造体と ShouldBind*、1つならアクセサを直接呼ぶ、で十分です。なお Bind*Should が付かない方)は失敗時に自動で 400 を書き込んでしまい、レスポンス形式を制御できません。API を書くなら ShouldBind* を選んでください。

v1.12.0 では、URI とクエリのバインドが encoding.UnmarshalText を実装した独自型を認識するようになりました。ID を専用の型で表現している設計では、変換コードを1つ減らせます。

エラー収集とpanic回復の実装、GetErrorの誤解

c.Errorで積んだエラーの取り出し方

Gin はハンドラやミドルウェアの途中で発生したエラーを、コンテキストに積んでおく仕組みを持っています。処理を止めずにエラーだけ記録し、最後にまとめてログへ流す用途です。

r.GET("/collect", func(c *gin.Context) {
	c.Error(fmt.Errorf("cache miss"))
	c.Error(fmt.Errorf("fallback failed"))
	c.JSON(200, gin.H{
		"errors":  len(c.Errors),           // 2
		"strings": c.Errors.Errors(),       // ["cache miss","fallback failed"]
		"last":    c.Errors.Last().Error(), // "fallback failed"
	})
})

手元で実行すると、そのとおり {"errors":2,"last":"fallback failed","strings":["cache miss","fallback failed"]} が返りました。c.Error は積むだけでレスポンスもステータスコードも変えません。積んだ内容を実際に出力するのは、最後段のロギングミドルウェアの役目です。ここを実装し忘れると、c.Error の呼び出しは何も起こさない飾りになります。

Recovery通過後のレスポンスとGetErrorの正体

gin.Default() に含まれる Recovery ミドルウェアは、ハンドラ内の panic を捕まえてプロセスの停止を防ぎます。panic("db down") を起こすハンドラを叩くと、標準エラー出力にスタックトレースが記録され、クライアントには本文が空の 500 が返りました。Gin が「クラッシュフリー」と称するのはこの挙動を指します。逆に言えば、panic の内容はクライアントには一切伝わりません。ユーザー向けのメッセージが必要なら、panic に頼らず明示的にエラーレスポンスを書いてください。

v1.12.0 の変更履歴には「add GetError and GetErrorSlice methods for error retrieval」という項目があり、名前から c.Errors を取り出すメソッドだと読めます。これは誤りです。実際の GetErrorfunc (c *Context) GetError(key any) error という署名で、c.Set で保存した任意の値を error 型として取り出す汎用アクセサです。c.Set("dberr", err) しておけば c.GetError("dberr") で取れますが、c.Error で積んだものは取れません(キーが存在しなければ nil が返ります)。エラー収集の取り出しは従来どおり c.Errors を直接参照してください。

本番投入前に必ず変える2つの設定(release mode・信頼プロキシ)

ここが日本語の入門記事でほぼ抜け落ちる部分です。上位に出る解説記事を実際に読み比べても、SetTrustedProxies に触れているものは見当たらず、GIN_MODE も起動ログの貼り付けに登場するだけで設定として説明されていません。既定のまま本番へ出す構成が残りやすい理由がここにあります。変えるべきは2つです。

GIN_MODE=releaseによるデバッグ出力の停止

既定の debug モードでは、登録した全ルートとハンドラ名が起動時に標準出力へ列挙されます。内部構造を運用ログに書き出しているのと同じで、望ましくありません。GIN_MODE=release を環境変数で渡すか、コードで gin.SetMode(gin.ReleaseMode) を呼びます。同じバイナリを GIN_MODE=release 付きで起動して /ping を叩いたところ、出力は次の1行だけになりました。

[GIN] 2026/08/11 - 01:04:49 | 200 | 76.022µs |       127.0.0.1 | GET      "/ping"

[GIN-debug] 行がすべて消え、アクセスログだけが残ります。環境変数で切り替えるほうを推奨します。コードに SetMode を直書きすると、開発環境でもデバッグ出力が得られなくなるためです。

SetTrustedProxiesによるX-Forwarded-For偽装の遮断

起動ログの「You trusted all proxies, this is NOT safe.」がこれを指しています。Gin は既定で X-Forwarded-For ヘッダをすべて信用し、c.ClientIP() がその値を返します。ロードバランサの背後に置くなら便利な既定値です。しかし信頼範囲を絞らなければ、クライアントが偽装した X-Forwarded-For がそのまま本物の IP として通ります。IP ベースのレート制限やアクセス制限を ClientIP() で判定している場合、その制限は素通りです。

r := gin.Default()

// ロードバランサのIPレンジだけを信頼する
if err := r.SetTrustedProxies([]string{"10.0.0.0/8"}); err != nil {
	log.Fatal(err)
}

// プロキシを経由しない構成なら、信頼を完全に切る
// r.SetTrustedProxies(nil)

プロキシを挟まずに直接インターネットへ晒すなら SetTrustedProxies(nil) で切ってください。クラウドのロードバランサ配下なら、そのレンジだけを列挙します。AWS の ALB や Google Cloud のように専用ヘッダを持つ環境では、EngineTrustedPlatform フィールドにプラットフォーム定数を設定する方法もあります。どれか1つは必ず選んでください。既定のままが最悪の選択です。

JSONエンコーダ差し替えによる応答生成の実測短縮

sonicへの差し替えで実際に縮んだ時間

Gin は JSON のエンコーダをビルドタグで差し替えられます。既定は標準ライブラリの encoding/json ですが、ByteDance の sonic、goccy/go-json、json-iterator に切り替えられます。ソースの codec/json/sonic.go のビルド制約は sonic && (linux || windows || darwin) で、対応 OS であればタグを1つ足すだけです。

効果を確かめるため、4フィールドの構造体100要素を c.JSON で返すハンドラを用意し、同一環境で3回ずつ計測しました(Go 1.26.5 darwin/amd64、Intel Core i9-9880H、Gin v1.12.0、間接依存の sonic は v1.15.0、go test -bench -benchtime=2s -count=3、GOMAXPROCS は既定の16、httptest 経由)。

ビルド方法 1回あたり 確保メモリ 確保回数
既定(encoding/json 102,283〜102,710 ns 約8.25 KB 3
go build -tags=sonic 39,837〜39,998 ns 約8.52 KB 4

約2.6倍の短縮です。1リクエストあたり約62マイクロ秒が消えた計算になります。引き換えにヒープ確保はわずかに増え、1リクエストあたり約270バイト・1回分多くなりました。同じ環境で測った経路解決の差(後述する約0.7マイクロ秒)と比べると、桁が2つ違います。Gin アプリの応答時間を削りたいなら、ルーターの選び直しより先にエンコーダを見るべきだ、というのがこの数字の意味です。

導入の前提と、入れないほうがよい条件

ただし無条件には勧めません。sonic はアセンブリによる最適化を含み、対応アーキテクチャの外では性能特性が変わります。ビルドタグを追加する以上、CI とデプロイ先の両方で同じタグを付ける運用が必要です。レスポンスが数キロバイト以下の API では約62マイクロ秒の差は誤差に埋もれるので、一覧系エンドポイントが重いと分かってから導入すれば十分です。

Echo・chi・Fiberとの選定基準

フレームワーク選定で要るのは機能表ではなく決め手です。2026年8月時点の各プロジェクトの状況と、選定の軸を整理します。

名称 最新版 公開日 性格
Gin v1.12.0 2026-02-28 ルーター+バインド+描画
Echo v5.3.1 2026-07-21 同等機能+標準装備が厚い
chi v5.3.1 2026-07-06 net/http準拠のルーターのみ
Fiber v3.4.0 2026-07-02 fasthttp基盤・独自API

表の「最新版」は各系列の最上位バージョンです。これとは別に保守枝のリリースが続いており、chi は v5.2.4 が2026年7月22日、Fiber は v2 系の v2.52.14 が2026年7月6日に出ています。表の日付だけを見て開発が止まったと判断しないでください。

判断軸は、標準ライブラリとの互換性・標準装備の厚さ・基盤の3系統に分かれます。標準ライブラリとの互換性を最優先するなら chi です。ハンドラの型が http.HandlerFunc そのままなので、既存のミドルウェア資産がそのまま動きます。Gin と Echo は独自の Context 型を持つため、net/http 向けに書かれたミドルウェアはラップが必要です。

バインドとバリデーションを標準装備で使いたいなら Gin か Echo です。この2つは機能面でほぼ等価で、決め手になるのは周辺です。Echo は v5 系へ移行しており、モジュールパスが github.com/labstack/echo/v5 に変わりました(v4 系も v4.15.4 が2026年6月に出ており、当面は併存します)。移行コストを負いたくなければ Gin のほうが安定しています。日本語の実装例も Gin のほうが見つけやすい状況です。

Fiber は勧めません。基盤が net/http ではなく fasthttp のため、標準ライブラリを前提としたライブラリや http.Handler 互換のミドルウェアが使えません。ベンチマーク上の数字は魅力的に見えますが、エコシステムから外れる代償が大きく、その差は先に実測したとおりエンコーダやI/Oに簡単に埋もれます。fasthttp 固有の性能特性が必要だと計測で示せる場合を除いて、選ぶ理由がありません。

Ginを選ばない判断:標準ServeMuxで足りる条件

Go 1.22 で http.ServeMux にメソッド指定とパスパラメータが入りました。これによって、かつて「Gin を入れる理由」の筆頭だった機能が標準ライブラリに来ています。Go コミュニティでも、この変更以降 Gin を外した開発者がいるかどうかがr/golang の議論スレッドで交わされています。以下、標準で足りる範囲を実際に動かして確かめます。

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, `{"id":%q}`, r.PathValue("id"))
})
log.Fatal(http.ListenAndServe(":8080", mux))

手元で動かすと、GET /users/42 は 200 で {"id":"42"}、階層が余分な GET /users/42/extra は 404、登録していない POST /users/42 は 405 が返り、Allow: GET, HEAD ヘッダまで自動で付きました。Gin では 405 を返すのに HandleMethodNotAllowed を有効化する必要があるので、この点はむしろ標準のほうが素直です。

経路解決の実測差と、その差が応答時間に占める割合

20本のルートを登録し、パスパラメータ2個を含む経路(/groups/123/roles/456)への解決を3回ずつ計測しました(Go 1.26.5 darwin/amd64、Intel Core i9-9880H、Gin v1.12.0、go test -bench -benchtime=3s -count=3、httptest 経由)。追試する場合は、httptest.NewRequesthttptest.NewRecorder をループ外で1つ作って使い回してください。ループ内で毎回 NewRecorder を生成すると、記録器自身の確保が計上されて Gin 側も 200 B 超・4 allocs になり、下表のゼロアロケーションは再現しません。

ルーター 1回あたり 確保メモリ 確保回数
Gin v1.12.0 217.2〜218.9 ns 0 B 0
http.ServeMux(Go 1.26.5) 919.4〜944.3 ns 48 B 2

Gin が約4.2〜4.3倍速く、ヒープ確保はゼロです。基数木とゼロアロケーション設計が効いていることは数字で確認できました。ただし差の絶対値は1リクエストあたり約0.7マイクロ秒です。DB へ1回問い合わせれば数百マイクロ秒から数ミリ秒かかります。この計測はネットワークもハンドラ処理も含まないプロセス内の経路解決だけを取り出したものです。応答時間が1ミリ秒のエンドポイントなら、ルーターの差が占める割合は0.07%にすぎません。なお ns/op の絶対値は計測ランごとに1〜2割振れます(別ランでは Gin 245〜256 ns、ServeMux 1,079〜1,086 ns)。ランをまたいで安定するのは比とアロケーション回数のほうで、結論はどちらで読んでも変わりません。速度を理由に Gin を選ぶのは、この数字を見る限り成り立ちません。

依存を増やす対価に見合う規模の分かれ目

では何が判断材料になるのでしょうか。標準 ServeMux に無いのは、ルートグループ、ミドルウェアの連結機構、構造体タグによるバインドとバリデーション、そして c.JSON のような描画ヘルパーです。これらを自前で書くと、結局は小さなフレームワークを作ることになります。

したがって、次の条件に当てはまるなら Gin を入れないでください。エンドポイントが十数本程度で、認証やロギングのミドルウェアが2つ3つに収まり、リクエストボディの検証も encoding/json と手書きのチェックで済んでいる規模です。この場合、依存を1つ増やす対価に見合いません。Gin v1.12.0 は間接依存として sonic、quic-go、validator、mongo-driver などを引き込みます。Go のバージョン要求(1.25 以上)に縛られる点も、CI を持つチームでは見落とせないコストです。なお標準ライブラリ側で組む場合、1つのポートで gRPC と HTTP を同居させたいといった要件は、Go開発者が知るべきcmuxの役割と接続マルチプレクサの基本動作原理で扱う接続多重化で解決できます。

逆に、REST API のエンドポイントが数十本あり、バージョン別のルートグループを切り、入力検証を宣言的に書きたいなら Gin が効きます。判断の分かれ目は性能ではなく、バインドとグループ化を自前で書くコストです。

よくある質問

gin.Default()とgin.New()は何が違いますか?

gin.Default() は Logger と Recovery の2つのミドルウェアを最初から組み込んだ Engine を返します。gin.New() は何も組み込まれていない素の Engine です。手元で Handlers の本数を数えると、前者が2本、後者が0本でした。本番では構造化ログを自前のミドルウェアで出したいことが多いので、gin.New()gin.Recovery() と独自ロガーを Use する構成が扱いやすくなります。Recovery だけは必ず入れてください。panic がプロセス全体を落とします。

GinでSQLやORMはどう使いますか?

Gin にデータベース機能はありません。database/sql か ORM を別途組み合わせます。接続プールはアプリ起動時に1つ作り、ハンドラからはそれを共有するのが基本形です。ハンドラごとに接続を開く実装は、負荷時に接続数が上限へ張り付く原因です。Go でよく使われる GORM のジェネリクス対応 API についてはGORMジェネリクスAPIを活用した基本的な実装パターンで扱っています。同じキーへの問い合わせが同時多発する構成なら、singleflightとは何か?Go言語における重複呼び出し抑制メカニズムの概要とパフォーマンス向上効果の手法と組み合わせると DB への負荷を抑えられます。

PostgreSQLの「GINインデックス」とGinフレームワークは関係ありますか?

まったく別物です。PostgreSQL の GIN は Generalized Inverted Index の略で、配列や JSONB、全文検索の転置索引を指します。読みも「ジン」で綴りも同じですが、Go の Web フレームワークとは無関係です。検索時に gin index と入力すると両方が混ざって出てくるので、目的に応じて postgresql gin indexgo gin router のように文脈語を足すと絞り込めます。

v1.11.0から1.12.0へ上げるときの注意点は?

最大の変更は最小 Go バージョンが 1.25 になったことです(v1.11.0 の go.modgo 1.23.0、v1.12.0 は go 1.25.0)。公式リリースアナウンスにも「最小サポート Go バージョンは 1.25」と記載があります。ビルド環境と CI の Go を先に上げてください。API 面では追加が中心で、GetErrorGetErrorSliceDelete といった Context メソッド、BSON の描画対応、Protocol Buffers のコンテントネゴシエーション対応、URI・クエリバインドでの encoding.UnmarshalText 対応が入りました。既存コードの書き換えを強制する変更は含まれていないため、Go のバージョンさえ揃えば移行自体は軽い部類です。

ファイルアップロードはどう実装しますか?

c.FormFilemultipart.FileHeader を受け取り、c.SaveUploadedFile で保存します。メモリに載せる上限は r.MaxMultipartMemory(既定32MiB)で、超えた分は一時ファイルへ退避されます。手元で15バイトの CSV を送ると 200 と {"name":"report.csv","size":15} が返り、ファイルも保存されました。失敗時の c.FormFile のエラーは原因ごとに文言が変わります。フォーム自体は届いているが指定した名前のファイル欄が無い場合は http: no such file、ボディが空の場合は multipart: NextPart: EOF、そもそも multipart/form-data で送られていない場合は request Content-Type isn't multipart/form-data です。エラー文字列で分岐せず、いずれも 400 として扱ってください。保存先パスを組み立てる際は filepath.Base でファイル名を正規化し、クライアントが指定した名前をそのまま連結しないでください。

関連記事

資料請求

RELATED POSTS 関連記事