
Gin徹底解説:Golangで最も注目されるフレームワーク

GinはGo(Golang)で書かれたHTTPウェブフレームワークです。Martiniに似たAPIを備えており、しかしMartiniよりも最大40倍速いパフォーマンスを持っています。素晴らしいパフォーマンスが必要なら、Ginを使ってみてください。

Ginの公式ウェブサイトでは、自身を「高性能」と「高い生産性」を持つウェブフレームワークと紹介しています。また、他の2つのライブラリにも言及しています。1つ目はMartiniで、これもウェブフレームワークで、お酒の名前を持っています。GinはそのAPIを利用しているが、40倍速いと述べています。`httprouter`を使用することが、Martiniより40倍速くなる重要な理由の1つです。
公式ウェブサイトの「特徴」の中で、8つの主要な特徴が挙げられており、後でこれらの特徴の実装を段階的に見ていきます。
Fast
Middleware support
Crash-free
JSON検証
JSON validation
Routes grouping
Error management
Rendering built-in/Extendable
小さな例から始める
公式ドキュメントに記載されている最小の例を見てみましょう。
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 0.0.0.0:8080でリッスンしてサーブする
}
この例を実行して、ブラウザを使って`http://localhost:8080/ping`にアクセスすると、「pong」が表示されます。
この例は非常にシンプルです。3つのステップに分けることができます:
`gin.Default()`を使って、デフォルト設定の`Engine`オブジェクトを作成する。
`Engine`の`GET`メソッドで「/ping」アドレスに対するコールバック関数を登録する。この関数は「pong」を返す。
`Engine`を起動して、ポートをリッスンしてサービスを提供する。
HTTP Method
上記の小さな例の`GET`メソッドから分かるように、GinではHTTPメソッドの処理メソッドは同じ名前の対応する関数を使って登録する必要があります。
HTTPメソッドには9つあり、最も一般的に使用される4つは`GET`、`POST`、`PUT`、`DELETE`で、それぞれ照会、挿入、更新、削除の4つの機能に対応しています。注意すべきは、Ginは`Any`インターフェースも提供しており、これはすべてのHTTPメソッドの処理メソッドを1つのアドレスに直接バインドできます。
返される結果には一般的に2つまたは3つの部分が含まれます。`code`と`message`は常に存在し、`data`は一般的に追加データを表します。追加データが返されない場合は省略できます。例では、200は`code`フィールドの値で、「pong」は`message`フィールドの値です。
Engine変数の作成
上記の例では、`gin.Default()`を使って`Engine`を作成しました。ただし、この関数は`New`のラッパーです。実際には、`Engine`は`New`インターフェースを通じて作成されます。
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
//... RouterGroupのフィールドを初期化する
},
//... 残りのフィールドを初期化する
}
engine.RouterGroup.engine = engine // EngineのポインタをRouterGroupに保存する
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
今は作成プロセスを簡単に見ておき、`Engine`構造体内の様々なメンバー変数の意味には注目しません。`New`は`Engine`型の`engine`変数を作成および初期化するだけでなく、`engine.pool.New`を`engine.allocateContext()`を呼び出す匿名関数に設定していることがわかります。この関数の機能については後で説明します。
ルートコールバック関数の登録
`Engine`内には埋め込み構造体`RouterGroup`があります。`Engine`のHTTPメソッドに関連するインターフェースはすべて`RouterGroup`から継承されています。公式ウェブサイトで言及されている特徴点の「ルートグループ化」は`RouterGroup`構造体を通じて実現されています。
type RouterGroup struct {
Handlers HandlersChain // グループ自体の処理関数
basePath string // 関連するベースパス
engine *Engine // 関連するエンジンオブジェクトを保存する
root bool // ルートフラグ、Engineでデフォルトで作成されるものだけがtrue
}
各`RouterGroup`はベースパス`basePath`と関連付けられています。`Engine`に埋め込まれている`RouterGroup`の`basePath`は「/」です。
また、一連の処理関数`Handlers`もあります。このグループに関連するパス下のすべてのリクエストは、このグループの処理関数を追加で実行します。これらは主にミドルウェア呼び出しに使用されます。`Engine`が作成されるとき、`Handlers`は`nil`で、`Use`メソッドを通じて一連の関数をインポートできます。この使い方を後で見ていきます。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
`RouterGroup`の`handle`メソッドは、すべてのHTTPメソッドコールバック関数を登録するための最終的なエントリポイントです。最初の例で呼び出された`GET`メソッドや他のHTTPメソッドに関連するメソッドは、`handle`メソッドのラッパーに過ぎません。
`handle`メソッドは`RouterGroup`の`basePath`と相対パスパラメータに基づいて絶対パスを計算し、同時に`combineHandlers`メソッドを呼び出して最終的な`handlers`配列を取得します。これらの結果は`Engine`の`addRoute`メソッドにパラメートとして渡され、処理関数を登録します。
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
`combineHandlers`メソッドは、`mergedHandlers`というスライスを作成し、それに`RouterGroup`自身の`Handlers`をコピーし、次にパラメータの`handlers`をコピーし、最後に`mergedHandlers`を返します。つまり、`handle`を使って任意のメソッドを登録するとき、実際の結果には`RouterGroup`自身の`Handlers`が含まれます。
基数木を使ったルート検索の高速化
公式ウェブサイトの「高速」という特徴点では、ネットワークリクエストのルーティングは基数木(Radix Tree)に基づいて実装されていると述べられています。この部分はGinによって実装されているのではなく、最初のGinの紹介で言及された`httprouter`によるものです。Ginは`httprouter`を使ってこの部分の機能を実現しています。基数木の実装については今は触れず、現時点ではその使い方に焦点を当てます。もしかすると、後で基数木の実装に関する別の記事を書くかもしれません。
`Engine`内には`trees`という変数があり、これは`methodTree`構造体のスライスです。すべての基数木への参照を保持しているのはこの変数です。
type methodTree struct {
method string // メソッド名
root *node // リンクリストのルートノードへのポインタ
}
`Engine`は各HTTPメソッドに対して基数木を維持しています。この木のルートノードとメソッド名は`methodTree`変数に一緒に保存され、すべての`methodTree`変数は`trees`にあります。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
//... 一部のコードを省略
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
//... 一部のコードを省略
}
`Engine`の`addRoute`メソッドでは、まず`trees`の`get`メソッドを使って`method`に対応する基数木のルートノードを取得します。基数木のルートノードが取得できない場合は、この`method`に対して以前にメソッドが登録されていないことを意味し、木ノードが作成されて木のルートノードとして`trees`に追加されます。
ルートノードを取得した後、ルートノードの`addRoute`メソッドを使ってパス`path`に対する一連の処理関数`handlers`を登録します。このステップでは、`path`と`handlers`に対するノードを作成して基数木に格納します。すでに登録されているアドレスを登録しようとすると、`addRoute`は直接`panic`エラーを投げます。
HTTPリクエストを処理するとき、`path`を通じて対応するノードの値を見つける必要があります。ルートノードには`getValue`メソッドがあり、これは照会操作を処理します。GinがHTTPリクエストを処理するときにこれについて触れます。
Middleware処理関数のインポート
`RouterGroup`の`Use`メソッドを使って、一連のミドルウェア処理関数をインポートできます。公式ウェブサイトで言及されている特徴点の「ミドルウェアサポート」は`Use`メソッドを通じて実現されています。
最初の例では、`Engine`構造体変数を作成するとき、`New`ではなく`Default`を使いました。`Default`が追加で何をしているか見てみましょう。
func Default() *Engine {
debugPrintWARNINGDefault() // ログを出力する
engine := New() // オブジェクトを作成する
engine.Use(Logger(), Recovery()) // ミドルウェア処理関数をインポートする
return engine
}
これは非常にシンプルな関数です。`New`を呼び出して`Engine`オブジェクトを作成する以外に、`Use`を呼び出して2つのミドルウェア関数`Logger`と`Recovery`の戻り値をインポートしています。`Logger`の戻り値はログ記録用の関数で、`Recovery`の戻り値は`panic`を処理する関数です。これについては今はスキップし、後でこれら2つの関数を見ていきます。
`Engine`は`RouterGroup`を埋め込んでいますが、`Use`メソッドも実装しています。ただし、これは`RouterGroup`の`Use`メソッドを呼び出していくつかの補助操作を行っているだけです。
func (engine *Engine) Use(middleware...HandlerFunc) IR
# 続き
```go
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
上記のコードから分かるように、`RouterGroup`の`Use`メソッドも非常にシンプルです。それは単に`append`を使って、パラメータのミドルウェア処理関数を自身の`Handlers`に追加するだけです。
起動する
小さな例では、最後のステップは`Engine`の`Run`メソッドを引数なしで呼び出すことです。呼び出した後、フレームワーク全体が起動し、ブラウザで登録済みのアドレスにアクセスすると、コールバックが正しくトリガーされます。
func (engine *Engine) Run(addr...string) (err error) {
//...一部のコードを省略
address := resolveAddress(addr) // アドレスを解析する。デフォルトアドレスは0.0.0.0:8080
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
`Run`メソッドは2つのことを行います:アドレスを解析してサービスを起動することです。ここでは、アドレスは実際には1つの文字列で渡せばいいのですが、渡すか渡さないかの両方の効果を実現するために、可変長引数を使用しています。`resolveAddress`メソッドは`addr`の様々な状況の結果を処理します。
サービスを起動する際には、標準ライブラリの`net/http`パッケージの`ListenAndServe`メソッドを使用します。このメソッドは、リッスンアドレスと`Handler`インターフェース型の変数を受け取ります。`Handler`インターフェースの定義は非常にシンプルで、`ServeHTTP`という1つのメソッドだけです。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
`Engine`は`ServeHTTP`を実装しているため、ここでは`Engine`自身が`ListenAndServe`メソッドに渡されます。監視しているポートに新しい接続があると、`ListenAndServe`が接続を受け入れて確立し、接続にデータがあると、`handler`の`ServeHTTP`メソッドが呼び出されて処理されます。
メッセージを処理する
`Engine`の`ServeHTTP`はメッセージを処理するコールバック関数です。その内容を見てみましょう。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
このコールバック関数には2つのパラメータがあります。1つ目は`w`で、これはリクエストの返信を受け取るために使用され、返信データは`w`に書き込まれます。もう1つは`req`で、これはこのリクエストのデータを保持しており、後続の処理に必要なすべてのデータは`req`から読み取ることができます。
`ServeHTTP`メソッドは4つのことを行います。まず、`pool`プールから`Context`を取得し、次に`Context`をコールバック関数のパラメータにバインドし、その後`Context`を引数として`handleHTTPRequest`メソッドを呼び出してこのネットワークリクエストを処理し、最後に`Context`をプールに戻します。
まずは`handleHTTPRequest`メソッドの核心部分だけ見てみましょう。
func (engine *Engine) handleHTTPRequest(c *Context) {
//...一部のコードを省略
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method!= httpMethod {
continue
}
root := t[i].root
// 木の中からルートを探す
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
//...一部のコードを省略
if value.handlers!= nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//...一部のコードを省略
}
//...一部のコードを省略
}
`handleHTTPRequest`メソッドは主に2つのことを行います。まず、リクエストのアドレスに基づいて、以前に登録されたメソッドを基数木から取得します。ここで、`handlers`はこの処理のための`Context`に割り当てられ、その後`Context`の`Next`関数を呼び出して`handlers`内のメソッドを実行します。最後に、このリクエストの返信データを`Context`の`responseWriter`型オブジェクトに書き込みます。
Context
HTTPリクエストを処理するとき、すべてのコンテキスト関連データは`Context`変数にあります。作者も`Context`構造体のコメントで「Contextはginの最も重要な部分です」と書いており、その重要性がわかります。
先ほど`Engine`の`ServeHTTP`メソッドについて話したとき、`Context`は直接作成されるのではなく、`Engine`の`pool`変数の`Get`メソッドを通じて取得されることがわかります。取り出した後、使用前にその状態がリセットされ、使用後にプールに戻されます。
`Engine`の`pool`変数は`sync.Pool`型です。今のところ、Goの公式が提供する、並行使用をサポートするオブジェクトプールであることだけ知っておけばいいです。`Get`メソッドを使ってプールからオブジセクトを取得でき、`Put`メソッドを使ってオブジェクトをプールに戻すことができます。プールが空で`Get`メソッドを使用すると、自身の`New`メソッドを通じてオブジェクトを作成して返します。
この`New`メソッドは`Engine`の`New`メソッドで定義されています。もう一度`Engine`の`New`メソッドを見てみましょう。
func New() *Engine {
//...他のコードを省略
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
コードから分かるように、`Context`の作成方法は`Engine`の`allocateContext`メソッドです。`allocateContext`メソッドには特に難しいところはありません。単にスライス長の事前割り当てを2段階で行い、その後オブジェクトを作成して返します。
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
先ほど述べた`Context`の`Next`メソッドは`handlers`内のすべてのメソッドを実行します。その実装を見てみましょう。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
`handlers`はスライスですが、`Next`メソッドは単純に`handlers`のトラバースとして実装されているのではなく、処理進捗記録`index`を導入しています。これは初期値が0で、メソッドの最初でインクリメントされ、メソッドの実行が完了すると再度インクリメントされます。
`Next`の設計はその使い方と大きく関係しており、主にいくつかのミドルウェア関数と協調するためです。例えば、ある`handler`の実行中に`panic`がトリガーされた場合、ミドルウェア内で`recover`を使ってエラーをキャッチし、その後`Next`を再呼び出すことで、1つの`handler`の問題で全体の`handlers`配列に影響を与えることなく、後続の`handlers`を続けて実行できます。
Panicを処理する
Ginでは、あるリクエストの処理関数が`panic`をトリガーした場合、フレームワーク全体が直接クラッシュすることはありません。代わりにエラーメッセージが投げられ、サービスは引き続き提供されます。これは、Luaのフレームワークが通常`xpcall`を使ってメッセージ処理関数を実行するのと少し似ています。これが公式ドキュメントで言及されている「Crash-free」という特徴点です。
前述の通り、`gin.Default`を使って`Engine`を作成するとき、`Engine`の`Use`メソッドが実行されて2つの関数がインポートされます。その1つは`Recovery`関数の戻り値で、これは他の関数のラッパーです。最終的に呼び出される関数は`CustomRecoveryWithWriter`です。この関数の実装を見てみましょう。
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
//...他のコードを省略
return func(c *Context) {
defer func() {
if err := recover(); err!= nil {
//...エラー処理コード
}
}()
c.Next() // 次のhandlerを実行する
}
}
ここではエラー処理の詳細には注目せず、単に何をしているか見てみましょう。この関数は匿名関数を返します。この匿名関数内で、`defer`を使って別の匿名関数が登録されています。この内側の匿名関数では、`recover`を使って`panic`をキャッチし、その後エラー処理が行われます。処理が終わった後、`Context`の`Next`メソッドが呼び出され、元々順番に実行されていた`Context`の`handlers`を続けて実行できます。
Leapcell:ウェブホスティング、非同期タスク、およびRedis用の次世代サーバレスプラットフォーム
最後に、Ginサービスを展開するための最適なプラットフォームであるLeapcellを紹介します。

1. 多言語サポート
JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料で展開
使用量に応じて支払います。リクエストがなければ、料金はかかりません。
3. 比類なきコスト効率
使った分だけ支払い、アイドル料金はありません。
例:25ドルで平均応答時間60ミリ秒の694万件のリクエストをサポートします。
4. 簡素化された開発者体験
直感的なUIで簡単なセットアップが可能です。
完全自動化されたCI/CDパイプラインとGitOps統合。
リアルタイムのメトリクスとログで実行可能な洞察を提供します。
5. 簡単なスケーラビリティと高性能
自動スケーリングで高い並列性を簡単に処理できます。
オペレーションオーバーヘッドはゼロです。ビルドに集中できます。
Docsでもっと詳細を探索してください。
Leapcell Twitter:https://x.com/LeapcellHQ