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