GoのhttpサーバにおけるMiddlewareとは
記事一覧はこちら
背景・モチベーション
go-chi
を好んで使うのですが、ふんわりとした理解でMiddlewareを宣言していました。Middlewareという名称も個人的にはあんまり腑に落ちていなくて、最近少し気になって調べたところ、「ロギングや認証などの横断的な関心事を処理するデザインパターン」を指すようです。NodeJSやRailsでもMiddlewareと呼称するようで、Javaサーブレットではフィルタ、C#ではデリゲートハンドラーと呼ばれているそう(後者2つには馴染みがある)。
このへんでしっかりGoでのMiddlewareの実装を確認しておこうと思いました。最近Goを使っていなかったので、コードリーディングしがてらリハビリをばという気持ちもあります。
参考文献
GoのHTTPサーバの処理の流れ
まずGoでHTTPサーバを公開するときはどのようなコードになるでしょうか。 下記はGoDocのサンプルコードから拝借したコードです。シンプルな実装だとこのようになると思います。
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サーバを公開するときに実行するhttp.ListenAndServe(addr string, handler Handler) error という関数から見ていきます。
処理の一部を省略したり、改変することで見通しを良くしています。関数の呼び出しに関してもインライン展開している箇所があります。
// https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L3155-L3165 func ListenAndServe(addr string, handler Handler) error { srv := &Server{Addr: addr, Handler: handler} // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2890-L2911 ln, err := net.Listen("tcp", addr) // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2941-L3015 for { rw, _ := ln.Accept() c := srv.newConn(rw) go func(ctx context.Context) { // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L1816-L1983 for { w, _ := c.readRequest(ctx) serverHandler{c.server}.ServeHTTP(w, w.req) } }(connCtx) } } // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2879-L2888 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler // このHandlerは、ListenAndServe の第2引数 if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) // ここでユーザが定義した関数を呼んでいる! }
追っていくと、ユーザが呼び出したhttp.ListenAndServe(addr string, handler Handler)
のときの第2引数にあたるhandlerからServeHTTP(ResponseWriter, *Request)
というメソッドを呼んでいます。このメソッドでユーザで定義したコードが実行されるようになっています。
「最初のコードでServeHTTP(ResponseWriter, *Request)
を定義していないけど?」と思いますが、今回はhandlerの引数に nil
を渡しています。そのため、 DefaultServeMux
というhandlerが利用されることになります。これは ServeMux
という構造体の型です。この構造体は、 http.Handler
のinterfaceを実装しています。
「ではDefaultServeMux
はどこで触ったのか?」という疑問が浮かびます。次はサンプル実装で呼んでいた http.HandleFunc
に注目してみたいと思います。
下記のコードに出てくる Mux
という名称は multiplexer
の略で、複数の入力を受け取り、それらを選択したりまとめたりして一つを出力する装置のことだそうです。コードコメントでも、ServeMux
は、httpリクエストのマルチプレクサで、受け取ったリクエストを照合し処理を呼び出すとあります。
// https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2218-L2258 type ServeMux struct { m map[string]struct{ h Handler pattern string } } var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2509-L2514 func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { e := muxEntry{h: HandlerFunc(handler), pattern: pattern} DefaultServeMux.m[pattern] = e // DefaultServeMux に登録している } // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2061-L2070 type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L62-L88 type Handler interface { ServeHTTP(ResponseWriter, *Request) }
このように http.HandleFunc
では、DefaultServeMux
にHandlerを登録する仕組みになっています。引数として渡した関数は、 HandlerFunc
というstructに変換されて Handler
というinterfaceを実装する型になります。このinterfaceは、 ServeHTTP(w, r)
というメソッドを持ちます。
最後にどのようにHandlerが呼び出されているかを確認してみます。 関数と一緒に引数に渡したpatternにmatchするかどうかを見て、ユーザの処理コードを実行しています。
// https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2437-L2449 // https://github.com/golang/go/blob/go1.16/src/net/http/server.go#L2368-L2435 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { v, _ := mux.m[r.URL.Path] v.h.ServeHTTP(w, r) }
Middlewareの実装について
GoにおけるMiddlewareは別のHandlerをwrapするHandlerです。ListenAndServe から呼び出されるように登録されていて、呼び出されると前処理を行い、その後にWrapしているHandlerを呼び出します。
上記で見てきた、 ServeMux
もMiddlewareです。Handlerのinterfaceを実装していて、Pathパターンを見て正しいユーザコードのHandlerを呼び出す前処理を行っていました。
では、 go-chi
でのロギングのMiddlewareはどのように実装されているか見ていこうと思います。
go-chi
を利用したシンプルなサーバ実装例は下記です。前セクションとは違って、 http.ListenAndServe(addr, handler)
の第2引数に nil
ではなく chi.Mux
というHandlerを実装した構造体を渡しています。つまり、 DefaultServeMux
ではなく、ここで渡したHandlerの ServeHTTP(w, r)
が呼ばれることになります。
func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) http.ListenAndServe(":3000", r) }
それでは実際に、Middlewareを登録する処理である (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler)
を見ていきたいと思います。また、この後に関係するので、ユーザコードを登録している r.Get(pattern, func(w, r))
に関しても記載します。
前セクションと同じく、見通しを良くするために省略している箇所があります。
// https://github.com/go-chi/chi/blob/v5.0.1/mux.go#L13-L46 type Mux struct { // The middleware stack middlewares []func(http.Handler) http.Handler } // https://github.com/go-chi/chi/blob/v5.0.1/middleware/logger.go#L23-L43 func Logger(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { /* ログ出力処理 */ }() next.ServeHTTP(ww, WithLogEntry(r, entry)) } return http.HandlerFunc(fn) } // https://github.com/go-chi/chi/blob/v5.0.1/mux.go#L92-L103 func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { mx.middlewares = append(mx.middlewares, middlewares...) } // https://github.com/go-chi/chi/blob/v5.0.1/mux.go#L145-L149 // https://github.com/go-chi/chi/blob/v5.0.1/mux.go#L384-L407 func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { // ミドルウェアをチェインさせる if !mx.inline && mx.handler == nil { mx.handler = middlewares[len(middlewares)-1](endpoint) for i := len(middlewares) - 2; i >= 0; i-- { mx.handler = middlewares[i](mx.handler) } } return mx.tree.InsertRoute(method, pattern, h) }
Middlewareとして渡している型が func(http.Handler) http.Handler
であることに注意してください。 Logger
関数を見ていただければ分かりますが、ロギング処理と引数で渡したhandlerを実行する関数を返しています。
Mux
構造体の middlewares
スライスに詰められた後は、 Get(pattern, handleFn)
が呼び出されたタイミングで、登録した順にMiddlewareがchainして最後にユーザコードが呼び出されるようなHandlerを生成しています。
それでは、実際にどのように呼ばれているかを (mx *Mux) ServeHTTP(w, r)
で確認してみたいと思います。
// https://github.com/go-chi/chi/blob/v5.0.1/mux.go#L58-L90 func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) // Serve the request and once its done, put the request context back in the sync pool mx.handler.ServeHTTP(w, r) mx.pool.Put(rctx) }
上記のように、 Mux
構造体のhandlerフィールドの ServeHTTP(w, r)
が実行されています。厳密ではないかもしれませんが、イメージとしては下記です。
Middlewareに関しては、自前で実装することも多いようで、昔の書籍にはなりますが「Go言語によるWebアプリケーション開発」というオライリーから出ている書籍で自分も写経した記憶があります。
コードリーディングしてまとめるのは疲れますが、自分のためになりますね。 Goに対するモチベが上がってきたので、途中で投げたままのアプリケーションを再開させようかなと思いました!以上!