トライ木をベースにしたGo製のHTTP Routerです。
このロゴはgopherize.meで作成しました。
- Go1.20 >= 1.16
- トライ木をベースとしたシンプルなデータ構造
- 軽量
- Lines of codes:2428
- Package size: 140K
- 標準パッケージ以外の依存性なし
- net/httpとの互換性
- net/httpのServemuxよりも高機能
- メソッドベースのルーティング
- 名前付きパラメータのルーティング
- 正規表現を使ったルーティング
- ミドルウェア
- カスタム可能なエラーハンドラー
- デフォルトOPTIONSハンドラー
- 0allocs
- 静的なルーティングにおいて0allocsを達成
- 名前付きルーティングについては3allocs程度
- パラメータのslice生成やパラメータをcontextに格納する部分でヒープ割当が発生
go get -u github.com/bmf-san/goblin
サンプルの実装を用意しています。
example_goblin_test.goをご参照ください。
任意のHTTPメソッドに基づいてルーティングを定義することができます。
以下のHTTPメソッドをサポートしています。
GET/POST/PUT/PATCH/DELETE/OPTIONS
r := goblin.NewRouter()
r.Methods(http.MethodGet).Handler(`/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/")
}))
r.Methods(http.MethodGet, http.MethodPost).Handler(`/methods`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
fmt.Fprintf(w, "GET")
}
if r.Method == http.MethodPost {
fmt.Fprintf(w, "POST")
}
}))
http.ListenAndServe(":9999", r)
名前付きパラメータ(:paramName
)を使ったルーティングを定義することができます。
r := goblin.NewRouter()
r.Methods(http.MethodGet).Handler(`/foo/:id`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := goblin.GetParam(r.Context(), "id")
fmt.Fprintf(w, "/foo/%v", id)
}))
r.Methods(http.MethodGet).Handler(`/foo/:name`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := goblin.GetParam(r.Context(), "name")
fmt.Fprintf(w, "/foo/%v", name)
}))
http.ListenAndServe(":9999", r)
名前付きパラメータに正規表現を使うこと(:paramName[pattern]
)で正規表現を使ったルーティングを定義することができます。
r.Methods(http.MethodGet).Handler(`/foo/:id[^\d+$]`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := goblin.GetParam(r.Context(), "id")
fmt.Fprintf(w, "/foo/%v", id)
}))
リクエストの前処理、レスポンスの後処理に役立つミドルウェアをサポートしています。
任意のルーティングに対してミドルウェアを定義することができます。
グローバルにミドルウェアを設定することもできます。グローバルにミドルウェアを設定すると、すべてのルーティングにミドルウェアが適用されるようになります。
ミドルウェアは1つ以上設定することができます。
ミドルウェアはhttp.Handlerを返す関数として定義する必要があります。
// http.Handlerを返す関数としてミドルウェアを実装
func global(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "global: before\n")
next.ServeHTTP(w, r)
fmt.Fprintf(w, "global: after\n")
})
}
func first(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "first: before\n")
next.ServeHTTP(w, r)
fmt.Fprintf(w, "first: after\n")
})
}
func second(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "second: before\n")
next.ServeHTTP(w, r)
fmt.Fprintf(w, "second: after\n")
})
}
func third(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "third: before\n")
next.ServeHTTP(w, r)
fmt.Fprintf(w, "third: after\n")
})
}
r := goblin.NewRouter()
// グローバルにミドルウェアを設定
r.UseGlobal(global)
r.Methods(http.MethodGet).Handler(`/globalmiddleware`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/globalmiddleware\n")
}))
// Useメソッドを使用することでミドルウェアを適用できます
r.Methods(http.MethodGet).Use(first).Handler(`/middleware`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "middleware\n")
}))
// ミドルウェアは複数設定することができます
r.Methods(http.MethodGet).Use(second, third).Handler(`/middlewares`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "middlewares\n")
}))
http.ListenAndServe(":9999", r)
/globalmiddleware
にリクエストすると、次のような結果が得られます。
global: before
/globalmiddleware
global: after
/middleware
にリクエストすると、次のような結果が得られます。
global: before
first: before
middleware
first: after
global: after
/middlewares
にリクエストすると、次のような結果が得られます。
global: before
second: before
third: before
middlewares
third: after
second: after
global: after
独自のエラーハンドラーを定義することができます。
定義可能なエラーハンドラは以下の2種類です。
- NotFoundHandler
- ルーティングにマッチする結果が得られなかったときに実行されるハンドラです
- MethodNotAllowedHandler
- マッチするメソッドがなかった場合に実行されるハンドラです
func customMethodNotFound() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "customMethodNotFound")
})
}
func customMethodAllowed() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "customMethodNotAllowed")
})
}
r := goblin.NewRouter()
r.NotFoundHandler = customMethodNotFound()
r.MethodNotAllowedHandler = customMethodAllowed()
http.ListenAndServe(":9999", r)
OPTIONSメソッドでのリクエストの際に実行されるデフォルトのハンドラを定義することができます。
func DefaultOPTIONSHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
}
r := goblin.NewRouter()
r.DefaultOPTIONSHandler = DefaultOPTIONSHandler()
http.ListenAndServe(":9999", r)
デフォルトOPTIONSハンドラーは例えば、CORSのOPTIONSリクエスト(preflight request)の対応などに役立ちます。
goblinのベンチマークテストを実行するコマンドを用意しています。
Makefileをご参照ください。
他のHTTP Routerとのベンチマーク比較結果が気になりますか?
こちらをご覧ください! bmf-san/go-router-benchmark
goblinの内部的なデータ構造について解説します。
パフォーマンスが最適化されたHTTP Routerにおいては、基数木が採用されていることが多いですが、goblinはトライ木をベースとしたデータ構造を採用しています。
基数木と比較すると、トライ木はメモリ使用量に劣る為、パフォーマンス面では不利です。しかしアルゴリズムの単純さ、理解しやすさは圧倒的にトライ木に軍配が上がるでしょう。
HTTP Routerは一見単純な仕様を持つアプリケーションに思えるかもしれませんが、意外と複雑です。これはテストケースを見て頂ければわかるかと思います。 (もっと良い感じのテストケースの実装アイデアがあればぜひ教えてください。)
単純なアルゴリズムを採用していることのメリットとしては、コードのメンテナビリティに貢献するという点です。(基数木の実装の難しさに対する言い訳とも聞こえるかもしれません・・実際のところ基数木をベースにしたHTTP Routerの実装の難しさには一度挫折しました・・)
_examplesのソースコードを例に、goblinの内部的なデータ構造について説明します。
ルーティングの定義を表で表すと、次のようになります。
Method | Path | Handler | Middleware |
---|---|---|---|
GET | / | RootHandler | N/A |
GET | /foo | FooHandler | CORS |
POST | /foo | FooHandler | CORS |
GET | /foo/bar | FooBarHandler | N/A |
GET | /foo/bar/:name | FooBarNameHandler | N/A |
POST | /foo/:name | FooNameHandler | N/A |
GET | /baz | BazHandler | CORS |
gobinではこのようなルーティングは次のような木構造として表現されます。
凡例:<HTTP Method>,[Node]
<GET>
├── [/]
|
├── [/foo]
| |
| └── [/bar]
| |
| └── [/:name]
|
└── [/baz]
<POST>
└── [/foo]
|
└── [/:name]
HTTPメソッドごとに木を構築するようになっています。
各ノードはハンドラーやミドルウェアの定義をデータとして持っています。
ここでは説明を簡素にするため、名前付きルーティングのデータや、グローバルミドルウェアのデータなどを省略しています。
内部で構築される木には他にも色々なデータが保持されます。
詳しく知りたい場合はデバッカーを使って内部構造を覗いてみてください。
改善のアイデアがあればぜひ教えてください!
参考資料の一覧はwikiに記載しています。
IssueやPull Requestはいつでもお待ちしています。
気軽にコントリビュートしてもらえると嬉しいです。
コントリビュートする際は、以下の資料を事前にご確認ください。
もし気に入って頂けたのならスポンサーしてもらえると嬉しいです! GitHub Sponsors - bmf-san
あるいはstarを貰えると嬉しいです!
継続的にメンテナンスしていく上でのモチベーションになります :D
MITライセンスに基づいています。
- Blog