🔥

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

2024/12/28に公開

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つのステップに分けることができます:

  1. gin.Default()を使って、デフォルト設定のEngineオブジェクトを作成する。
  2. EngineGETメソッドで「/ping」アドレスに対するコールバック関数を登録する。この関数は「pong」を返す。
  3. Engineを起動して、ポートをリッスンしてサービスを提供する。

HTTP Method

上記の小さな例のGETメソッドから分かるように、GinではHTTPメソッドの処理メソッドは同じ名前の対応する関数を使って登録する必要があります。
HTTPメソッドには9つあり、最も一般的に使用される4つはGETPOSTPUTDELETEで、それぞれ照会、挿入、更新、削除の4つの機能に対応しています。注意すべきは、GinはAnyインターフェースも提供しており、これはすべてのHTTPメソッドの処理メソッドを1つのアドレスに直接バインドできます。
返される結果には一般的に2つまたは3つの部分が含まれます。codemessageは常に存在し、dataは一般的に追加データを表します。追加データが返されない場合は省略できます。例では、200はcodeフィールドの値で、「pong」はmessageフィールドの値です。

Engine変数の作成

上記の例では、gin.Default()を使ってEngineを作成しました。ただし、この関数はNewのラッパーです。実際には、EngineNewインターフェースを通じて作成されます。

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構造体内の様々なメンバー変数の意味には注目しません。NewEngine型のengine変数を作成および初期化するだけでなく、engine.pool.Newengine.allocateContext()を呼び出す匿名関数に設定していることがわかります。この関数の機能については後で説明します。

ルートコールバック関数の登録

Engine内には埋め込み構造体RouterGroupがあります。EngineのHTTPメソッドに関連するインターフェースはすべてRouterGroupから継承されています。公式ウェブサイトで言及されている特徴点の「ルートグループ化」はRouterGroup構造体を通じて実現されています。

type RouterGroup struct {
    Handlers    HandlersChain // グループ自体の処理関数
    basePath    string        // 関連するベースパス
    engine      *Engine       // 関連するエンジンオブジェクトを保存する
    root        bool          // ルートフラグ、Engineでデフォルトで作成されるものだけがtrue
}

RouterGroupはベースパスbasePathと関連付けられています。Engineに埋め込まれているRouterGroupbasePathは「/」です。
また、一連の処理関数Handlersもあります。このグループに関連するパス下のすべてのリクエストは、このグループの処理関数を追加で実行します。これらは主にミドルウェア呼び出しに使用されます。Engineが作成されるとき、Handlersnilで、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()
}

RouterGrouphandleメソッドは、すべてのHTTPメソッドコールバック関数を登録するための最終的なエントリポイントです。最初の例で呼び出されたGETメソッドや他のHTTPメソッドに関連するメソッドは、handleメソッドのラッパーに過ぎません。
handleメソッドはRouterGroupbasePathと相対パスパラメータに基づいて絶対パスを計算し、同時にcombineHandlersメソッドを呼び出して最終的なhandlers配列を取得します。これらの結果はEngineaddRouteメソッドにパラメートとして渡され、処理関数を登録します。

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)
    //... 一部のコードを省略
}

EngineaddRouteメソッドでは、まずtreesgetメソッドを使ってmethodに対応する基数木のルートノードを取得します。基数木のルートノードが取得できない場合は、このmethodに対して以前にメソッドが登録されていないことを意味し、木ノードが作成されて木のルートノードとしてtreesに追加されます。
ルートノードを取得した後、ルートノードのaddRouteメソッドを使ってパスpathに対する一連の処理関数handlersを登録します。このステップでは、pathhandlersに対するノードを作成して基数木に格納します。すでに登録されているアドレスを登録しようとすると、addRouteは直接panicエラーを投げます。
HTTPリクエストを処理するとき、pathを通じて対応するノードの値を見つける必要があります。ルートノードにはgetValueメソッドがあり、これは照会操作を処理します。GinがHTTPリクエストを処理するときにこれについて触れます。

Middleware処理関数のインポート

RouterGroupUseメソッドを使って、一連のミドルウェア処理関数をインポートできます。公式ウェブサイトで言及されている特徴点の「ミドルウェアサポート」はUseメソッドを通じて実現されています。
最初の例では、Engine構造体変数を作成するとき、NewではなくDefaultを使いました。Defaultが追加で何をしているか見てみましょう。

func Default() *Engine {
    debugPrintWARNINGDefault()       // ログを出力する
    engine := New()                  // オブジェクトを作成する
    engine.Use(Logger(), Recovery()) // ミドルウェア処理関数をインポートする
    return engine
}

これは非常にシンプルな関数です。Newを呼び出してEngineオブジェクトを作成する以外に、Useを呼び出して2つのミドルウェア関数LoggerRecoveryの戻り値をインポートしています。Loggerの戻り値はログ記録用の関数で、Recoveryの戻り値はpanicを処理する関数です。これについては今はスキップし、後でこれら2つの関数を見ていきます。
EngineRouterGroupを埋め込んでいますが、Useメソッドも実装しています。ただし、これはRouterGroupUseメソッドを呼び出していくつかの補助操作を行っているだけです。

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()
}

上記のコードから分かるように、RouterGroupUseメソッドも非常にシンプルです。それは単にappendを使って、パラメータのミドルウェア処理関数を自身のHandlersに追加するだけです。

起動する

小さな例では、最後のステップはEngineRunメソッドを引数なしで呼び出すことです。呼び出した後、フレームワーク全体が起動し、ブラウザで登録済みのアドレスにアクセスすると、コールバックが正しくトリガーされます。

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)
}

EngineServeHTTPを実装しているため、ここではEngine自身がListenAndServeメソッドに渡されます。監視しているポートに新しい接続があると、ListenAndServeが接続を受け入れて確立し、接続にデータがあると、handlerServeHTTPメソッドが呼び出されて処理されます。

メッセージを処理する

EngineServeHTTPはメッセージを処理するコールバック関数です。その内容を見てみましょう。

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に割り当てられ、その後ContextNext関数を呼び出してhandlers内のメソッドを実行します。最後に、このリクエストの返信データをContextresponseWriter型オブジェクトに書き込みます。

Context

HTTPリクエストを処理するとき、すべてのコンテキスト関連データはContext変数にあります。作者もContext構造体のコメントで「Contextはginの最も重要な部分です」と書いており、その重要性がわかります。
先ほどEngineServeHTTPメソッドについて話したとき、Contextは直接作成されるのではなく、Enginepool変数のGetメソッドを通じて取得されることがわかります。取り出した後、使用前にその状態がリセットされ、使用後にプールに戻されます。
Enginepool変数はsync.Pool型です。今のところ、Goの公式が提供する、並行使用をサポートするオブジェクトプールであることだけ知っておけばいいです。Getメソッドを使ってプールからオブジセクトを取得でき、Putメソッドを使ってオブジェクトをプールに戻すことができます。プールが空でGetメソッドを使用すると、自身のNewメソッドを通じてオブジェクトを作成して返します。
このNewメソッドはEngineNewメソッドで定義されています。もう一度EngineNewメソッドを見てみましょう。

func New() *Engine {
    //...他のコードを省略
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}

コードから分かるように、Contextの作成方法はEngineallocateContextメソッドです。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}
}

先ほど述べたContextNextメソッドは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を作成するとき、EngineUseメソッドが実行されて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をキャッチし、その後エラー処理が行われます。処理が終わった後、ContextNextメソッドが呼び出され、元々順番に実行されていたContexthandlersを続けて実行できます。

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

Discussion