goblin

A golang http router based on trie tree.

MIT License

Stars
78
Committers
4

English 日本語

goblin

トライ木をベースにした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ハンドラー

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

参考資料の一覧はwikiに記載しています。

コントリビューション

IssueやPull Requestはいつでもお待ちしています。

気軽にコントリビュートしてもらえると嬉しいです。

コントリビュートする際は、以下の資料を事前にご確認ください。

CODE_OF_CONDUCT CONTRIBUTING

スポンサー

もし気に入って頂けたのならスポンサーしてもらえると嬉しいです! GitHub Sponsors - bmf-san

あるいはstarを貰えると嬉しいです!

継続的にメンテナンスしていく上でのモチベーションになります :D

Stargazers

Forkers

ライセンス

MITライセンスに基づいています。

LICENSE

作者

bmf-san

Package Rankings
Top 5.08% on Proxy.golang.org
Badges
Extracted from project README's
Mentioned in Awesome Go GitHub release CircleCI Go Report Card codecov GitHub license Go Reference Sourcegraph Stargazers repo roster for @bmf-san/goblin Forkers repo roster for @bmf-san/goblin