Gin: Golangのリーディングフレームワークを深く掘り下げる
Min-jun Kim
Dev Intern · Leapcell

はじめに
Ginは、Go(Golang)で書かれたHTTPウェブフレームワークです。 MartiniのようなAPIを備えていますが、Martiniよりも最大40倍高速なパフォーマンスを発揮します。 圧倒的なパフォーマンスが必要な場合は、ぜひGinをお試しください。
Ginの公式サイトでは、Ginは「高性能」と「高い生産性」を備えたウェブフレームワークとして紹介されています。 また、他の2つのライブラリについても言及しています。 1つはMartiniで、これもウェブフレームワークであり、酒の名前でもあります。 Ginは、そのAPIを使用していますが、40倍高速であると述べています。 httprouter
を使用することは、Martiniよりも40倍高速である理由の重要な要因です。
公式サイトの「特徴」の中で、8つの重要な特徴が挙げられており、これらの特徴の実装について後ほど徐々に見ていきます。
- 高速
- ミドルウェアのサポート
- クラッシュフリー
- JSONバリデーション
- ルートのグループ化
- エラー管理
- レンダリング組み込み/拡張可能
小さな例から始める
公式ドキュメントに記載されている最小の例を見てみましょう。
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() // listen and serve on 0.0.0.0:8080 }
この例を実行し、ブラウザを使用してhttp://localhost:8080/ping
にアクセスすると、「pong」が返されます。
この例は非常にシンプルです。 わずか3つのステップに分割できます。
gin.Default()
を使用して、デフォルト設定のEngine
オブジェクトを作成します。Engine
のGET
メソッドで「/ping」アドレスのコールバック関数を登録します。 この関数は「pong」を返します。Engine
を起動して、ポートのリスンを開始し、サービスを提供します。
HTTPメソッド
上記の小さな例のGET
メソッドから、Ginでは、HTTPメソッドの処理メソッドは、同じ名前の対応する関数を使用して登録する必要があることがわかります。
9つのHTTPメソッドがあり、最も一般的に使用される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 // RouterGroupにengineのポインタを保存 engine.pool.New = func() any { return engine.allocateContext() } return engine }
今のところ、作成プロセスを簡単に見て、Engine
構造体のさまざまなメンバー変数の意味に焦点を当てないでください。 型Engine
のengine
変数の作成と初期化に加えて、New
はengine.pool.New
をengine.allocateContext()
を呼び出す匿名関数に設定することもわかります。 この関数の機能については後で説明します。
ルートコールバック関数の登録
Engine
には、埋め込み構造体RouterGroup
があります。 Engine
のHTTPメソッドに関連するインターフェースはすべて、RouterGroup
から継承されています。 公式ウェブサイトで言及されている機能ポイントの「ルートのグループ化」は、RouterGroup
構造体を介して実現されます。
type RouterGroup struct { Handlers HandlersChain // グループ自体の処理関数 basePath string // 関連付けられたベースパス engine *Engine // 関連付けられたエンジンオブジェクトを保存 root bool // rootフラグ、Engineでデフォルトで作成されたもののみがtrue }
各RouterGroup
は、ベースパスbasePath
に関連付けられています。 Engine
に埋め込まれたRouterGroup
のbasePath
は「/」です。
また一連の処理関数Handlers
があります。 このグループに関連付けられたパスの下のすべてのリクエストは、追加でこのグループの処理関数を実行します。これらは主にミドルウェアの呼び出しに使用されます。 Handlers
は、Engine
の作成時に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リクエストを処理する方法について説明するときに言及します。
ミドルウェア処理関数のインポート
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) 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つのことしか行いません。 ここで、アドレスは実際には文字列を渡すだけで済みますが、渡せるか渡せないかという効果を実現するために、可変長パラメータが使用されます。 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
型オブジェクトに書き込みます。
コンテキスト
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
の実行を継続できます。
パニックの処理
Ginでは、特定のリクエストの処理関数がpanic
をトリガーした場合、フレームワーク全体が直接クラッシュすることはありません。 代わりに、エラーメッセージがスローされ、サービスの提供が継続されます。 Luaフレームワークが通常xpcall
を使用してメッセージ処理関数を実行する方法といくらか似ています。 この操作は、公式ドキュメントで言及されている「クラッシュフリー」の機能ポイントです。
上記のように、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() // 次のハンドラを実行 } }
ここではエラー処理の詳細に焦点を当てず、何をするかのみを見てください。 この関数は匿名関数を返します。 この匿名関数では、別の匿名関数がdefer
を使用して登録されます。 この内部匿名関数では、recover
を使用してpanic
をキャッチし、エラー処理を実行します。 処理が完了すると、Context
のNext
メソッドが呼び出され、元々順番に実行されていたContext
のhandlers
を実行し続けることができます。
Leapcell: ウェブホスティング、非同期タスク、Redisのための次世代サーバーレスプラットフォーム
最後に、Ginサービスをデプロイするための最適なプラットフォームLeapcellを紹介しましょう。
1. 複数の言語をサポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストなし、料金なし。
3. 比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60msで694万リクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単な設定のための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとロギング。
5. 容易なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Leapcell Twitter: https://x.com/LeapcellHQ