Goのhttp Serverの雰囲気を理解する
Goのhttp serverの雰囲気を理解する
Goのhttpサーバの雰囲気を理解するための記事を書いた。
tl; dr
- Goのhttpサーバの肝は
Handlerインタフェースなので、Handlerインタフェースに注目すると雰囲気が掴みやすい。 Handlerインタフェースが、ライブラリやフレームワークを実現するための拡張性、柔軟性を提供している。(すごい)
Handlerインタフェースとは
code snippet start
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
code snippet end
第一引数にResponseWriter型、第二引数にRequestのポインタ型を取るServeHTTPというメソッドを実装している型を扱うためのインタフェース。
Goのhttpサーバは、このHandlerインタフェースを上手く使っている。
以下の説明で、Handlerインタフェースという文言は、標準パッケージのhttp.Handlerを指しているものとする。
始めてhttpサーバを立てるとき
ググってみて、以下のようなコードを書くはず。
go code snippet start
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)
}
}go code snippet end
HandleFuncメソッドやHandleメソッドを使ってhandlerを登録することができるのだな、handlerは特定のURLにマッピングされ、マッチするURLへのアクセス時にhandlerが実行されるのだな、と直感的に理解できると思う。
本格的に使おうとするとき
登録するhandlerの数が増えたり、より複雑になってくると、もっと上手くやれないものかと思うはず。例えば以下のようなもの。
- 様々なhandler間で共通の処理を上手く書けないか
- ロギング
- 認証
- Header情報に基づく共通処理、など
- もっと便利にroutingの設定をできないか
- 個別のHTTPメソッドに対してhandlerを登録したい
- path parameterを簡単にハンドリングしたい、など
サードパーティのライブラリやフレームワークを使えば、これらの要望を満たすことができる。では、サードパーティのライブラリやフレームワークは、何を、どのように実装しているのだろうか。それらの雰囲気を理解するために、まずは標準のhttpサーバの挙動の雰囲気を追ってみる。
標準ライブラリのhttpサーバの挙動
Goのソースコードから、雰囲気を汲み取ってみる。長ったらしいので、退屈であれば主要な構成要素とHandlerインタフェースまで読み飛ばしてもらってもいいと思う。
まずは、http.ListenAndServe(":8080", nil)の実行後の挙動を追う。
go code snippet start
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 }go code snippet end
Serverを初期化して、初期化したServerのListenAndServeメソッドを呼び出す。
ServerはHandlerインタフェースを持つが、これがすごく重要。
具象型ではなく、インタフェースになっているのがポイント。
go code snippet start
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 }go code snippet end
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が後の処理で使われるため。
go code snippet start
1830 serverHandler{c.server}.ServeHTTP(w, w.req)go code snippet end
func (c *conn) serve(ctx context.Context) {}の中のこの処理で、引き継がれたServerが持つHandlerインタフェースを使って、dispatchが始まる。
go code snippet start
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 }go code snippet end
2561行目のHandlerはinterfaceである。このHandlerは、http.ListenAndServe関数の第2引数として指定されたHandlerである。
Handlerインタフェースを満たしていればどんな型であってもよいので、以降はHandlerの実装により挙動が変わることになる(!)
ここからはhttp.ListenAndServe関数の第2引数をnilとした場合に使われるdefaultのDefaultServeMuxの挙動を追っていく。
DefaultServeMuxはServeMux型で、もちろんServeMux型はServeHTTPを実装しており、Handlerインタフェースを満たす。
go code snippet start
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 }go code snippet end
mux.Handler(r)で、事前に登録していたhandlerとURLのマッピング情報から実際にdispatchするhandlerを取得する。
go code snippet start
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 }go code snippet end
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]muxEntryにHandlerが登録してある |
Serverに呼び出される。URLにマッチするhandlerをm map[string]muxEntryから取得し、そのhandlerのServeHTTPメソッドを呼び出す。 |
http.muxEntryのHandler |
Handlerインタフェースを満たす |
ServeMuxに呼び出される。http.Handle()やhttp.HandleFuncで登録されるHandlerインタフェースの正体がこれ。 |
リクエストを受けてからhandlerがdispatchされるまでに、異なるレイヤで異なる働きをするものが実行されるが、レイヤ間の処理の受け渡しにHandlerインタフェースが使われていることがわかる。
つまり、ライブラリやフレームワークを実現するためには、Handlerを持つ、もしくはHandlerを満たすコンポーネントを開発してどこかのレイヤの実装を置き換える、または拡張すればよい。
なので、フレームワークやライブラリが何をやっているか知りたいときは、ServeHTTPでgrepしてみるとよい。ServeHTTPメソッドを読んでみて、どのレイヤの処理を置き換えたり拡張したりしているのか考えると雰囲気が掴みやすい。
ライブラリやフレームワークの雰囲気
middleware
middlewareは、前章のmuxEntryのHandlerのレイヤの処理の拡張を行うことで、様々な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/muxやjulienschmidt/httprouterのようなものがある。ServeMuxレイヤの処理を置き換えるので、ライブラリ独自のroutingを保持するデータ構造を持っていて、ServeHTTPメソッドの中で独自のroutingロジックを実装している。ここのroutingのロジックの実装をいい感じにすることで、個別のHTTPメソッドに対してhandlerを登録したり、path parameterのハンドリングができるようになる。
ちなみに、ServeMuxのレイヤの処理の実装を置き換えるので、それ以降のmuxEntryのHandlerレイヤの処理は必ずしもHandlerインタフェースを満たしている必要はない。ライブラリ側でシグネチャを変えることもできるし、もちろん変えなくてもよい。
その他
有名なフレームワークに、labstack/echoがある。このフレームワークでは、エントリポイントであるhttp.ServerをラップしたEchoというコンポーネントで全ての処理を置き換えている。
go code snippet start
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
}go code snippet end
TCPサーバとしてのServer型のみ利用しており、Server型からhttp.ServeMuxレイヤを呼び出すところのみHandlerインタフェースを使っているが、それ以外のところでは独自のシグネチャを採用している。
以下は、echoのページにあるサンプル。
code snippet start
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!")
}
code snippet end
httpサーバの起動はe.Start(":1323")という独自の実装となっている。また、echoへのhandlerの登録は、httpパッケージのHandlerインタフェースではなく、独自のシグネチャとなっていることがわかる。
まとめ
- Goでのhttpサーバの肝は
Handlerインタフェースなので、Handlerインタフェースを中心に考えると雰囲気が掴みやすい。 Handlerインタフェースの実装(=ServeHTTP)を追えば、各ライブラリやフレームワークが何をやっているか、雰囲気を掴める。Handlerというシンプルなインタフェースだけで拡張性や柔軟性をもたらしていて、すごい。