Goのhttp serverの雰囲気を理解する

Apr 30, 2018 00:21 · 4445 words · 9 minute read golang http

Goのhttp serverの雰囲気を理解する

Goのhttpサーバの雰囲気を理解するための記事を書いた。

tl; dr

  • Goのhttpサーバの肝はHandlerインタフェースなので、Handlerインタフェースに注目すると雰囲気が掴みやすい。
  • Handlerインタフェースが、ライブラリやフレームワークを実現するための拡張性、柔軟性を提供している。(すごい)

Handlerインタフェースとは

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

第一引数にResponseWriter型、第二引数にRequestのポインタ型を取るServeHTTPというメソッドを実装している型を扱うためのインタフェース。 Goのhttpサーバは、このHandlerインタフェースを上手く使っている。
以下の説明で、Handlerインタフェースという文言は、標準パッケージのhttp.Handlerを指しているものとする。

始めてhttpサーバを立てるとき

ググってみて、以下のようなコードを書くはず。

package main

import (
	"fmt"
	"net/http"
)

type fuga int

func (f *fuga) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "fuga type: %d", *f)
}

func main() {
	http.HandleFunc("/hoge", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello world")
	})

	f := fuga(1)
	http.Handle("/fuga", &f)

	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

HandleFuncメソッドやHandleメソッドを使ってhandlerを登録することができるのだな、handlerは特定のURLにマッピングされ、マッチするURLへのアクセス時にhandlerが実行されるのだな、と直感的に理解できると思う。

本格的に使おうとするとき

登録するhandlerの数が増えたり、より複雑になってくると、もっと上手くやれないものかと思うはず。例えば以下のようなもの。

  • 様々なhandler間で共通の処理を上手く書けないか
    • ロギング
    • 認証
    • Header情報に基づく共通処理、など
  • もっと便利にroutingの設定をできないか
    • 個別のHTTPメソッドに対してhandlerを登録したい
    • path parameterを簡単にハンドリングしたい、など

サードパーティのライブラリやフレームワークを使えば、これらの要望を満たすことができる。では、サードパーティのライブラリやフレームワークは、何を、どのように実装しているのだろうか。それらの雰囲気を理解するために、まずは標準のhttpサーバの挙動の雰囲気を追ってみる。

標準ライブラリのhttpサーバの挙動

Goのソースコードから、雰囲気を汲み取ってみる。長ったらしいので、退屈であれば主要な構成要素とHandlerインタフェースまで読み飛ばしてもらってもいいと思う。 まずは、http.ListenAndServe(":8080", nil)の実行後の挙動を追う。

2966 // ListenAndServe always returns a non-nil error.
2967 func ListenAndServe(addr string, handler Handler) error {
2968     server := &Server{Addr: addr, Handler: handler}
2969     return server.ListenAndServe()
2970 }

Serverを初期化して、初期化したServerListenAndServeメソッドを呼び出す。
ServerHandlerインタフェースを持つが、これがすごく重要。
具象型ではなく、インタフェースになっているのがポイント。

2697 // ListenAndServe listens on the TCP network address srv.Addr and then
2698 // calls Serve to handle requests on incoming connections.
2699 // Accepted connections are configured to enable TCP keep-alives.
2700 // If srv.Addr is blank, ":http" is used.
2701 // ListenAndServe always returns a non-nil error.
2702 func (srv *Server) ListenAndServe() error {
2703     addr := srv.Addr
2704     if addr == "" {
2705         addr = ":http"
2706     }
2707     ln, err := net.Listen("tcp", addr)
2708     if err != nil {
2709         return err
2710     }
2711     return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
2712 }

TCPサーバをListenしている。この後、func (srv *Server) Serve(l net.Listener) error {}func (c *conn) serve(ctx context.Context) {}が順次実行され、TCPサーバとしての役割を全うするがここでは割愛する。(長いので)

注目したいのは、始めにインスタンス化したServerインスタンスが後続の処理に引き継がれていること。
- func (srv *Server) Serve(l net.Listener) error {}のレシーバはServerのポインタ
- func (c *conn) serve(ctx context.Context) {}のレシーバはconnのポインタで、connはメンバとしてServerのポインタを保持している

なぜServerインスタンスが引き継がれることが重要かというと、Serverが持っているHandlerが後の処理で使われるため。

1830         serverHandler{c.server}.ServeHTTP(w, w.req)

func (c *conn) serve(ctx context.Context) {}の中のこの処理で、引き継がれたServerが持つHandlerインタフェースを使って、dispatchが始まる。

2560 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
2561     handler := sh.srv.Handler
2562     if handler == nil {
2563         handler = DefaultServeMux
2564     }
2565     if req.RequestURI == "*" && req.Method == "OPTIONS" {
2566         handler = globalOptionsHandler{}
2567     }
2568     handler.ServeHTTP(rw, req)
2569 }

2561行目のHandlerはinterfaceである。このHandlerは、http.ListenAndServe関数の第2引数として指定されたHandlerである。 Handlerインタフェースを満たしていればどんな型であってもよいので、以降はHandlerの実装により挙動が変わることになる(!)
ここからはhttp.ListenAndServe関数の第2引数をnilとした場合に使われるdefaultのDefaultServeMuxの挙動を追っていく。 DefaultServeMuxはServeMux型で、もちろんServeMux型はServeHTTPを実装しており、Handlerインタフェースを満たす。

2326 // ServeHTTP dispatches the request to the handler whose
2327 // pattern most closely matches the request URL.
2328 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
2329     if r.RequestURI == "*" {
2330         if r.ProtoAtLeast(1, 1) {
2331             w.Header().Set("Connection", "close")
2332         }
2333         w.WriteHeader(StatusBadRequest)
2334         return
2335     }
2336     h, _ := mux.Handler(r)
2337     h.ServeHTTP(w, r)
2338 }

mux.Handler(r)で、事前に登録していたhandlerとURLのマッピング情報から実際にdispatchするhandlerを取得する。

2133 type ServeMux struct {
2134     mu    sync.RWMutex
2135     m     map[string]muxEntry
2136     hosts bool // whether any patterns contain hostnames
2137 }
2138
2139 type muxEntry struct {
2140     h       Handler
2141     pattern string
2142 }

ServeMux型は、キーがURLパターン、バリューがHandlerを持つmuxEntry、なmap構造を持っており、ここにhandlerとURLのマッピング情報を保持している。
実はhttp.HandleFunc()http.Handle()では、このServeMux型のmにURLパターンとhandlerのマッピング情報が登録されている。

最後に取得したhandlerが実行される。

主要な構成要素とHandlerインタフェース

ここまで、雑に標準のhttpサーバの挙動を追ったが、ごちゃごちゃしているので主要な構成要素についてHandlerインタフェースとの関わりという観点で整理する。

Handlerインタフェースとの関わり 役割
http.Server Handlerインタフェースを持つ TCPサーバ。handlerのディスパッチが始まるエントリポイント。Request、Responseを生成した後、持っているHandlerインタフェースのServeHTTPメソッドを呼び出す。
http.ServeMux http.DefaultServeMuxの型 Handlerインタフェースを満たす。 m map[string]muxEntryHandlerが登録してある Serverに呼び出される。URLにマッチするhandlerをm map[string]muxEntryから取得し、そのhandlerのServeHTTPメソッドを呼び出す。
http.muxEntryHandler Handlerインタフェースを満たす ServeMuxに呼び出される。http.Handle()http.HandleFuncで登録されるHandlerインタフェースの正体がこれ。

リクエストを受けてからhandlerがdispatchされるまでに、異なるレイヤで異なる働きをするものが実行されるが、レイヤ間の処理の受け渡しにHandlerインタフェースが使われていることがわかる。

つまり、ライブラリやフレームワークを実現するためには、Handlerを持つ、もしくはHandlerを満たすコンポーネントを開発してどこかのレイヤの実装を置き換える、または拡張すればよい。

なので、フレームワークやライブラリが何をやっているか知りたいときは、ServeHTTPでgrepしてみるとよい。ServeHTTPメソッドを読んでみて、どのレイヤの処理を置き換えたり拡張したりしているのか考えると雰囲気が掴みやすい。

ライブラリやフレームワークの雰囲気

middleware

middlewareは、前章のmuxEntryHandlerのレイヤの処理の拡張を行うことで、様々なhandler間で共通の処理を上手く書くことできる。 具体的にどう実装するか、は以下の記事に詳しい説明がある。

Middlewares in Go: Best practices and examples
Making and Using HTTP Middleware
Writing HTTP Middleware in Go

Handlerインタフェースを利用した、Decoratorパターンで実現できる。

サードパーティだとgorilla/handlersのようなものがある。

router

routerは、前章のServeMuxのレイヤの処理の実装を別のコンポーネントに置き換える。 gorilla/muxjulienschmidt/httprouterのようなものがある。ServeMuxレイヤの処理を置き換えるので、ライブラリ独自のroutingを保持するデータ構造を持っていて、ServeHTTPメソッドの中で独自のroutingロジックを実装している。ここのroutingのロジックの実装をいい感じにすることで、個別のHTTPメソッドに対してhandlerを登録したり、path parameterのハンドリングができるようになる。

ちなみに、ServeMuxのレイヤの処理の実装を置き換えるので、それ以降のmuxEntryHandlerレイヤの処理は必ずしもHandlerインタフェースを満たしている必要はない。ライブラリ側でシグネチャを変えることもできるし、もちろん変えなくてもよい。

その他

有名なフレームワークに、labstack/echoがある。このフレームワークでは、エントリポイントであるhttp.ServerをラップしたEchoというコンポーネントで全ての処理を置き換えている。

Echo struct {
		stdLogger        *stdLog.Logger
		colorer          *color.Color
		premiddleware    []MiddlewareFunc
		middleware       []MiddlewareFunc
		maxParam         *int
		router           *Router
		notFoundHandler  HandlerFunc
		pool             sync.Pool
		Server           *http.Server
		TLSServer        *http.Server
		Listener         net.Listener
		TLSListener      net.Listener
		AutoTLSManager   autocert.Manager
		DisableHTTP2     bool
		Debug            bool
		HideBanner       bool
		HidePort         bool
		HTTPErrorHandler HTTPErrorHandler
		Binder           Binder
		Validator        Validator
		Renderer         Renderer
		Logger           Logger
	}

TCPサーバとしてのServer型のみ利用しており、Server型からhttp.ServeMuxレイヤを呼び出すところのみHandlerインタフェースを使っているが、それ以外のところでは独自のシグネチャを採用している。

以下は、echoのページにあるサンプル。

package main

import (
	"net/http"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	// Echo instance
	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routes
	e.GET("/", hello)

	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}

// Handler
func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

httpサーバの起動はe.Start(":1323")という独自の実装となっている。また、echoへのhandlerの登録は、httpパッケージのHandlerインタフェースではなく、独自のシグネチャとなっていることがわかる。

まとめ

  • Goでのhttpサーバの肝はHandlerインタフェースなので、Handlerインタフェースを中心に考えると雰囲気が掴みやすい。
  • Handlerインタフェースの実装(=ServeHTTP)を追えば、各ライブラリやフレームワークが何をやっているか、雰囲気を掴める。
  • Handlerというシンプルなインタフェースだけで拡張性や柔軟性をもたらしていて、すごい。

参考

Twitter Share