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

