包丁一本さらしに巻いて

net/httpで作るGo APIサーバー #3

2017.04.03

この一連の記事ではnet/httpを主軸に据え、取替可能な部品となるライブラリを利用してAPIサーバーを作成する方法を紹介する。

今回はhttp.Handlerを使い、HTTPリクエストの共通処理を記述するミドルウェアについて書く。

ミドルウェアとは

特にそういった構造体や関数が標準ライブラリの中にあるわけではない。Goではミドルウェアを連鎖させていくことでHTTPリクエストを処理する際に必要な共通処理(e.g. ロギング、認証、panicリカバリ、etc)を記述していく。

ミドルウェアとは実際に何かというと、func(http.Handler) http.Handlerというシグネチャを持ち、引数として渡ってきたhttp.HandlerServeHTTP(http.ResponseWriter, *http.Request)を内部で実行するhttp.Handlerインターフェースを満たすオブジェクトを返す関数、という感じ。http.Handlerとは何か、何が便利なのかはnet/httpで作るGo APIサーバー #2を読んで欲しい。

言葉で書くと長いのでロギング用のミドルウェアとpanicリカバリ用ミドルウェアのコードを書いた。

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t1 := time.Now()
		next.ServeHTTP(w, r)
		t2 := time.Now()
		t := t2.Sub(t1)
		log.Printf("[%s] %s %s", r.Method, r.URL, t.String())
	})
}

func recoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				debug.PrintStack()
				log.Printf("panic: %+v", err)
				http.Error(w, http.StatusText(500), 500)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

http.Handlerをチェインさせていくことで、ミドルウェアA->ミドルウェアB->ミドルウェアC->アプリケーションの処理、という形で書けるようになっている。矢印で表現するより本来は順序を持ってネストしていくイメージ。以下のような形でアプリケーションの処理とURLとの紐付けに利用する。

...
	r.Methods("GET").Path("/hello").Handler(
		recoverMiddleware(loggingMiddleware(AppHandler{h: app.Greeting})))
...

一点注意としては、ミドルウェアは必ず並べられた順に実行されるので、例えば認証ミドルウェアで作成したユーザー情報を利用するミドルウェアは必ず認証ミドルウェアの後に書かないと正常に動かない。

さらに見通しの良いミドルウェア

func(http.Handler) http.Handlerというシグネチャのシンプルな形は良いのだけど、標準ライブラリだけだとmiddlewareA(middlewareB(middlewareC(handler)))という形になってしまいmiddlewareをグループ化して使いまわしにくいし、何より不格好で視認性が低い。そこで、ミドルウェアのグルーピングやその他細かい事を良い感じに埋めてくれる薄いAliceというライブラリをお勧めしたい。Aliceを使うと以下のように書ける。

	// middleware chain
	chain := alice.New(
		recoverMiddleware,
		loggingMiddleware,
	)
	// for gorilla/mux
	router := mux.NewRouter()
	r := router.PathPrefix("/api").Subrouter()
	r.Methods("GET").Path("/hello").Handler(chain.Then(AppHandler{h: app.Greeting}))

だいぶ見通しがよくなった。また、Aliceはミドルウェア連鎖をグループ化しておけるので、例えば共通のミドルウェアチェーンはあるんだけど、publicなエンドポイントだけ別のミドルウェア群を挟みたい、privateなエンドポイントは認証を入れたい、みたいなことも綺麗に書ける。

	// middleware chain
	chain := alice.New(
		recoverMiddleware,
		loggingMiddleware,
	)
	publicChain := chain.Append(
		publicMiddleware,
	)
	privateChain := chain.Append(
		authMiddleware,
	)

ミドルウェア内でDBやアプリケーション設定を使いたい

net/httpで作るGo APIサーバー #1で説明したアプリケーショングローバルな値をミドルウェア内部で使いたくなることがある。特にDBにアクセスする認証や、アプリケーション設定内に記述した特定IPアドレスからのアクセスを拒否する、といった類のもの。func(http.Handler) http.Handlerだからそういう値を持ったミドルウェアは書けないのか、と自分は最初思ったが、特定の値(e.g. DBのコネクションやlogger)を取ってfunc(http.Handler) http.Handlerを返すクロージャーを使えば簡単に書ける。

以下のはアプリケーショングローバルなloggerをミドルウェア内で利用する例。

func appLoggingMiddleware(logger *log.Logger) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			t1 := time.Now()
			next.ServeHTTP(w, r)
			t2 := time.Now()
			t := t2.Sub(t1)
			logger.Printf("[%s] %s %s", r.Method, r.URL, t.String())
		})
	}
}

*log.Loggerを取ってfunc(http.Handler) http.Handlerを返す関数になってる。これを以下のように使う。

	// middleware chain
	chain := alice.New(
		recoverMiddleware,
		appLoggingMiddleware(app.Logger),
	)

こうすることで柔軟なミドルウェアを作成する事ができ、それらをグループ化して管理することで、どのエンドポイントはどのミドルウェアチェーンで処理されるのか明確になった。

ライブラリとしてのミドルウェア

このようにfunc(http.Handler) http.Handlerシグネチャを守りながらミドルウェアチェーンを構築すると、同じインターフェースに対応しているサードパーティ製のライブラリを組み込みやすくなる。例えば、以下に一覧がある。

便利なミドルウェアを書いてライブラリとして公開する場合もfunc(http.Handler) http.Handlerを守るのが良いと思う。

参考資料

このエントリーをはてなブックマークに追加
comments powered by Disqus