1クール続けるブログ

とりあえず1クール続けるエンジニアの備忘録

GoのhttpサーバにおけるMiddlewareとは

記事一覧はこちら

背景・モチベーション

go-chi を好んで使うのですが、ふんわりとした理解でMiddlewareを宣言していました。Middlewareという名称も個人的にはあんまり腑に落ちていなくて、最近少し気になって調べたところ、「ロギングや認証などの横断的な関心事を処理するデザインパターン」を指すようです。NodeJSやRailsでもMiddlewareと呼称するようで、Javaサーブレットではフィルタ、C#ではデリゲートハンドラーと呼ばれているそう(後者2つには馴染みがある)。

このへんでしっかりGoでのMiddlewareの実装を確認しておこうと思いました。最近Goを使っていなかったので、コードリーディングしがてらリハビリをばという気持ちもあります。

参考文献

eli.thegreenplace.net

github.com

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) が実行されています。厳密ではないかもしれませんが、イメージとしては下記です。

f:id:jrywm121:20210310231349p:plain

Middlewareに関しては、自前で実装することも多いようで、昔の書籍にはなりますが「Go言語によるWebアプリケーション開発」というオライリーから出ている書籍で自分も写経した記憶があります。

コードリーディングしてまとめるのは疲れますが、自分のためになりますね。 Goに対するモチベが上がってきたので、途中で投げたままのアプリケーションを再開させようかなと思いました!以上!