GoのHTTPサーバーにおけるURLとハンドラの紐付けについて考える

GoのHTTPサーバーにおけるURLとハンドラの紐付けについて疑問に思ったので調べてみました。Goでは比較的簡単にHTTPサーバーのコードを書くことができます。簡単に書けるために裏でどのような処理が行われているかわからないこともあります。 今回はHTTPサーバーの実装でどのようにURLとハンドラが紐づけれれているかについて調べました。

func main() {
	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
	})
	http.ListenAndServe(":8080", nil)
}

要約

HTTPサーバーの中身をみていく

Goのnet/httpパッケージを活用すると簡単にHTTPサーバーを構築することができます。 簡単なHTTPサーバーのコードは以下のような感じになります。

package main

import (
	"fmt"
	"html"
	"net/http"
)

func main() {
	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
	})
	http.ListenAndServe(":8080", nil)
}

このHTTPサーバーでは/barのURLにリクエストがきたらHello "/bar"という文字列を返すという単純なものです。 http.HabndleFuncでURLのパスとハンドラになる関数のセットを登録します。

実際にcurlで叩いてみると以下のようにリクエストが返ってきました。

$ curl localhost:8080/bar
Hello "bar"

どのようにこのハンドラが呼び出されているのかさらに調べてみます。

先ほどの例では最終行で、ListenAndServe関数を呼び出していました。 この関数は中でhttp.Server構造体を構築し、そのServer構造体のListenAndServeメソッドを呼び出します。

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

通常であれば、ListenAndServe関数は第二引数にHandler型の引数をとり、Server構造体のhanderにセットしています。 Handlerは以下のようなインターフェース型です。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

ServerHTTPのメソッドを持つものであればこのHadlerインターフェースを満たします。 リクエストがあった際にServer構造体のhandlerのServerHTTPを呼び出します。

試しに以下のコードを試してみます。

package main

import (
	"fmt"
	"net/http"
)

type MyInt int

func (m *MyInt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %d", *m)
}

func main() {
	myInt := MyInt(100)
	http.ListenAndServe(":8080", &myInt)
}

このコードでは最初にint型に対して、MyIntという名前をつけて、MyIntがHandlerインターフェースを満たすようにServeHTTPメソッドを定義します。 そして、main関数内でMyIntを初期化し、ListenAndServe関数の引数に初期化したmyIntを渡します。

以下はこの関数を実行し、HTTPサーバーに対して、curlコマンドでリクエストを投げてみます。

$ curl localhost:8080/
Hello "100"

$ curl localhost:8080/hoge
Hello "100"

$ curl localhost:8080/bar
Hello "100"

リクエストを投げてみると、期待通りHello “100”の文字列が返ってきます。 リクエストが送られてきたら、Server構造体のhandlerフィールドのServeHTTPを呼び出すことが確認できます。

しかしながら、どのURLに対してリクエストを送っても、Hello “100”が返ってきます。 では最初の本題に戻ります。どの部分でURLパスとハンドラの紐付けを行なっているのでしょうか。

ヒントはListenAndServe関数の第二引数で呼び出したときににあります。 最初の例ようにListenAndServe関数の第二引数にnilを指定するとどうなるのでしょうか。

ListenAndServe関数を辿っていくと、以下の関数に行き着きます。 (http.ListenAndSerev -> Server.ListenAndServe -> Server.Serve -> Conn.serve -> serverHandler.ServeHTTPというん順で辿る)

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}

関数内1行目handler := sh.srv.Handler handlerはListenAndServe関数で作成したServer構造体のフィールドhandlerを表しています。 最初の例のようにhttp.ListenAndServeの第二引数にnilを指定するとこの変数handlerはnilになり、上の関数の中でhandler = DefaultServeMuxが呼ばれます。 そのhandlerのServeHTTPコマンドが最終的に呼び出されます。

DefaultServeMuxはnet/httpパッケージ内のパッケージ変数として定義されています。

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

DefaultServeMux変数の正体はServeMux構造体のポインタです。

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {
	h       Handler
	pattern string
}

中略

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

DefaultServeMuxのServeHTTP関数では最終的にHandlerメソッドを呼び出して、その返り値に対してServeHTTPメソッドが呼び出されています。

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	// CONNECT requests are not canonicalized.
	if r.Method == "CONNECT" {
		// If r.URL.Path is /tree and its handler is not registered,
		// the /tree -> /tree/ redirect applies to CONNECT requests
		// but the path canonicalization does not.
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}

		return mux.handler(r.Host, r.URL.Path)
	}

	// All other requests have any port stripped and path cleaned
	// before passing to mux.handler.
	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	// If the given path is /tree and its handler is not registered,
	// redirect for /tree/.
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}

	return mux.handler(host, r.URL.Path)
}

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

頑張って辿っていくとmux.m[path]の関数を返していることがわかります。 DefaultServeMuxの中にはmap型があり、その中に事前にHandlerがあり、URLにマッチするHandlerをmapから取り出して、その関数のServeHTTP関数を呼び出していることになります。

このmap型はどのタイミングで初期化されHnadlerが登録されるのでしょうか。 その答えはhttp.HandleFuncにあります。

func main() {
	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
	})
	http.ListenAndServe(":8080", nil)
}

http.HandleFuncの中身は以下のようになっています。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMuxのHandleFuncメソッドが呼び出されています。

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

mux.Handle(pattern, HandlerFunc(handler))HadlerFunc(handler)は型変換です。 handlerをHandlerFunc型へと変換しています。

    var v float64 = 1.1
	intValue := int(v)
上の例のようにfloat64型からint型へ変換するのと同じ操作です。

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

HandlerFunc型はServeHTTPメソッドを持ちHandlerインターフェースを満たしています。 HandlerFunc型に型変換することによってHandlerインターフェースを満たすことになります。 mux.Handle(pattern, HandlerFunc(handler))のなかでは以下の操作を行なっています。

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
この部分でDefaultServeMux関数のmap型にURLパスとそのハンドラを紐づけています。

まとめると以下のようになります。

まとめ

長くなりましたが、自分の備忘録として残しておきます。