gorilla/muxのソースコードを読んだらめっちゃシンプルな仕組みだった
HTTPルータを書いたことはありますか?
Webサーバを作る時に使う、リクエスト(多くはHTTPメソッドやパス)に応じて処理を振り分けるやつです。
例えばこんな感じ(標準ライブラリのドキュメントから):
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
あるパスにマッチするかどうかを調べ、マッチした場合に特定のハンドラを起動する仕組みには、トライ木やその派生形のパトリシア木、基数木といったデータ構造がよく使われます。
トライ木は文字列などの値を複数保持する際に、共通部分を節、相違部分を葉とした木構造で、URLのパスから対応するハンドラを探すのに適しています。
例えば、以下の複数のパスを木構造にすると……
/login/
/users/
/users/alice/
/users/alice/followers/
/users/bob/
/about/
こうなります:
/
+- login/
+- users/
| +- alice/
| | +- followers/
| +- bob/
+- about/
トライ木を使った実装については以下の記事がとてもわかりやすかったのでオススメです。
Goのライブラリに関していえば、Ginやecho、chiなどのライブラリがそれにあたります(chiには「基数木の実装はarmon/go-radixを基にした」というコメントがありますね)。
いろいろあってこれらのコードを読んでいたところ、gorilla/muxがトライ木を使わないとてもシンプルな仕組みを採用していて驚いたので紹介します。
Router
gorilla/muxはGoの標準ライブラリであるnet/http
と組み合わせて使うことを想定しており、メインのルータであるRouter
がhttp.Handler
インターフェースを実装しています。
http.Handler
インターフェースはServeHTTP(w http.ResponseWriter, r *http.Request)
というメソッドただ1つを持つインターフェースです。
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
gorilla/muxではこんな感じで実装しています。
何をしているのかというと:
- パスをきれいにする
- 自身の
Match()
メソッドを呼び出してリクエストにマッチするルートRoute
があるか調べる - マッチしたルートがあれば、リクエストに追加の情報をセットしてそのルートに対応するハンドラを呼び出す
- 無ければ
notFoundHandler()
やmethodNotAllowedHandler()
など事前に定義済みのハンドラを呼び出す
という極めて自然なことをしています。
Router#Match()
上述の「2.」で呼び出されるRouter#Match()
はこれです。
保持しているRoute
を順に辿り、そのルートのMatch()
メソッドを呼び出してマッチするか調べています。
マッチした時の付随情報(ハンドラや変数など)はRouter#Match()
の第2引数であるRouteMatch
に格納するようになっており、あるルートがマッチした場合はそのハンドラにミドルウェアを適用しています。
Route#Match()
各ルートを表すRoute
はRoute#matchers
にMatcher
の配列を保持しています。
そしてRoute#Match()
が呼ばれると、Matcher#Match()
を呼び出してリクエストがマッチするか調べます。
Matcher
ではMatcher
はなんなのかというと、Match(*http.Request, *RouteMatch) bool
というメソッドを持つインターフェースです。
条件の対象となる項目はパスだけでなく、HTTPメソッドの種類やホスト名、スキーマ(http
とかhttps
とか)なども考えられます。
そのため、共通のインターフェースを定義して.それぞれの項目毎に実装となるMatcher
を用意しているわけです。
具体的な実装として、HTTPスキーマにマッチするschemeMatcher
や
ヘッダにマッチするheaderMatcher
/headerRegexpMatcher
そしてパスにマッチするrouteRegexp
などが用意されています。
作ってみよう
何かを理解するには車輪の再発明が有用です。作ってみましょう。
Matcher
まずはリクエストを受け取ってそのリクエストが事前に与えられた条件にマッチするか調べるMatcher
と、マッチした場合に使用する付随情報Matched
(gorilla/mux
におけるRouteMatch
)を考えましょう。
今回は単純に「パスを正規表現でマッチしたときのサブマッチ(つまり、/users/(?<name>[^/]+)
のname
部分)」を保持するVars
のみ定義しておきます。
type Matched struct {
Vars map[string]string
}
func NewMatched() *Matched {
return &Matched{
Vars: map[string]string{},
}
}
type Matcher interface {
Match(r *http.Request, matched *Matched) bool
}
例えば、HTTPメソッドが与えられた値ならマッチするMethodMatcher
はこうなります。
type MethodMatcher struct {
Method string
}
// Matcher implementation
func (m *MethodMatcher) Match(r *http.Request, matched *Matched) bool {
return r.Method == m.Method
}
さらに、パスが事前に与えられた正規表現とマッチするかチェックするRegexpMatcher
はこうです。
パスが正規表現にマッチしたら、名前付きのサブマッチの値をMatched.Vars
にセットします。
type RegexpMatcher struct {
*regexp.Regexp
}
// Matcher implementation
func (m *RegexpMatcher) Match(r *http.Request, matched *Matched) bool {
matches := m.Regexp.FindStringSubmatch(r.Url.Path)
if matches == nil {
return false
}
if matched != nil && matched.Vars != nil {
for i, name := range m.Regexp.SubexpNames() {
if name != "" {
matched.Vars[name] = matches[i]
}
}
}
return true
}
Route
これらMatcher
とハンドラを紐づけるRoute
構造体を定義します。
Route
はそのルートがリクエストとマッチするか調べるMatch
メソッドと、各種Matcher
やハンドラを登録するためのメソッドを持っています。
Matcher
やハンドラを登録するためのメソッドたちはメソッド・チェインできるようにRoute
自身を返すようにしました。
type Route struct {
matchers []Matcher
matched *Matched
Handler http.Handler
}
// 後で作るRouter.NewRoute()で作ってほしいため非公開にする
func newRoute() *Route {
return &Route{
matched: NewMatched(),
}
}
// Matcher implementation
func (r *Route) Match(req *http.Request) bool {
for _, m := range r.matchers {
if m.Match(req, r.matched) {
return true
}
}
return false
}
// パスにマッチ
func (r *Route) Path(re *regexp.Regexp) *Route {
r.matchers = append(r.matchers, &RegexpMatcher{re})
return r
}
// HTTPメソッドにマッチ
func (r *Route) Method(string name) *Route {
r.matchers = append(r.matchers, &MethodMatcher{name})
return r
}
// ハンドラを登録
func (r *Route) SetHandler(h http.Handler) *Route {
r.Handler = h
}
// http.Handler implementation
func(r *Route) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.Handler.ServeHTTP(w, req)
}
Router
さて、Route
は1つのルートを表す構造体ですから、リクエストが来たら複数のルートから一番最初にマッチしたルートに紐付けられたハンドラ呼び出すようにすればRouter
を作ることができます。
付随情報であるMatched
も後から使えるように保存しておきます。
マッチするルートがなかったときには専用のハンドラ(ここでは仮にnotFoundHandler
としましょう)を呼び出します。
type Router struct {
Matched *Matched
routes []*Route
notFoundHandler http.Handler
}
func NewRouter() *Router {
return &Router{
// notFoundHandlerの実装は省略
notFoundHandler: notFoundHandler,
}
}
// 新しくRouteを追加
func (r *Router) NewRoute() *Route {
route := newRoute()
r.routes = append(r.routes, route)
return route
}
// Routeが見つからなかった場合のハンドラを登録
func (r *Router) SetNotFoundHandler(h http.Handler) {
// nilだと困るので一応チェック
if h != nil {
r.notFoundHandler = h
}
}
// マッチするRouteを探す
func (r *Router) FindRoute(req *http.Request) *Route {
for _, route := range r.routes {
if route.Match(req) {
// 最初にマッチしたルートを返す
return route
}
}
return nil
}
// http.Handler implementation
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// マッチするルートを探してハンドラを呼ぶ
if route := r.FindRoute(req); route != nil {
r.Matched = route.Matched
route.ServeHTTP(w, req)
return
}
// 1つもマッチしなかった場合
r.notFoundHandler(w, req)
}
// 名前付きサブマッチの値を取得するユーティリティ
func (r *Router) Var(name string) (value string, ok bool) {
if r.Matched == nil {
return
}
value, ok = r.Matched.Vars[name]
return
}
SubRouter
新しくルータを作ってRoute#Handler
に設定すれば、あるルート以下をまとめるSubRouter
も作れます。
func (r *Route) SubRouter() *Router {
router := NewRouter()
route.SetHandler(router)
return router
}
簡単ですね。
Middleware
ミドルウェアもhttp.Handler
を受け取ってhttp.Handler
を返す型と考えると実は簡単です。
ルートやルーターにミドルウェアを複数持たせることができるようにし、ハンドラを呼ぶ時にミドルウェアを経由してあげれば実現できます。
// ミドルウェアを表すインターフェイス
type Middleware interface {
Apply(next http.Handler) http.Handler
}
// 関数でもMiddlewareインターフェイスを満たせるようにする
type MiddlewareFunc func(next http.Handler) http.Handler
func (f MiddlewareFunc) Apply(next http.Handler) http.Handler {
return f(next)
}
// 複数のミドルウェアを適用したハンドラを返す
func applyMiddlewares(next http.Handler, middlewares []Middleware) http.Handler {
h := next
for _, m := range middlewares {
h = m.Apply(h)
}
return h
}
Route
ごとのミドルウェア:
type Route struct {
middlewares []Middleware
// 省略
}
func (r *Route) Use(m Middleware) *Router {
r.middlewares = append(r.middlewares, m)
return r
}
// http.Handler implementation
func (r *Route) ServeHTTP(w http.ResponseWriter, req *http.Request) {
handler := applyMiddlewares(r.Handler, r.middlewares)
handler.ServeHTTP(w, req)
}
Router
ごとのミドルウェア:
type Router struct {
middlewares []Middleware
// 省略
}
func (r *Router) Use(m Middleware) *Router {
r.middlewares = append(r.middlewares, m)
return r
}
// http.Handler implementation
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// マッチするルートを見つける
if route := r.FindRoute(req); route != nil {
r.Matched = route.Matched
handler := applyMiddlewares(route, r.middlewares)
handler.ServeHTTP(w, req)
}
// 1つもマッチしなかった場合
r.notFoundHandler(w, req)
}
完成!
ここまでくると、こんな感じでそれっぽく使うことができるルータができました!
import "net/http"
// sample middleware
func Logging(next http.Handler) http.Handler {
return func(w http.ResponseWriter, req *http.Request) {
log.Printf("Request: %v\n", req)
next(w, req)
}
}
func main() {
// ルータを作る
r := NewRouter()
// ミドルウェアを追加
r.Use(Logging)
// ルートを定義する
r.NewRoute().Method("GET").Path("/users/(?<user>[^/]+)/?").SetHandler(func (w http.ResponseWriter, req *http.Request) {
user, ok := r.Var("user")
if !ok {
user = "Guest"
}
io.WriteString(w, "Hello, " + user + "!")
})
// ListenAndServeを呼ぶなりなんなりする
http.ListenAndServe(":8080", r)
}
Discussion