Go勉強会 Webアプリケーション編 #3 でやろうと思っている Go Web Examples: Middleware (Advanced) の和訳です。
Middleware: Advanced
この例では Go でもうミドルウェアのもう少し高度なバージョンを作る方法を示しましょう。
ミドルウェアそれ自体は単純に http.HandlerFunc
を引数の一つとして受け取り、それをラップし、サーバーが呼び出すための新しい http.HandlerFunc
を返します。
ここでは複数のミドルウェアを簡単にチェインできるようにすることを目的として新しい Middleware
型を定義します。このアイディアは May Rayer の Buidling APIs に関する発表から着想をえたものです。より詳しい説明はここの発表に含まれています。
このコード片で新しいミドルウェアがどのように作られるか詳しく説明します。後述の完全な例では幾らかのボイラープレートコードでこのバージョンを短くしています。
func createNewMiddleware() Middleware {
// 新しい Middleware を作る。
middleware := func(next http.HandlerFunc) http.HandlerFunc {
// 最終的にサーバーから呼び出される http.HandlerFunc を定義する。
handler := func(w http.ResponseWriter, r *http.Request) {
// ... ミドルウェアとしての処理を行う。
// チェインの次のミドルウェア/HTTP ハンドラを呼び出す。
next(w, r)
}
// 新しく作った HTTP ハンドラを返す。
return handler
}
// 新しく作ったミドルウェアを返す。
return middleware
}
完全な例
// advanced-middleware.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
// Logging
// 全てのリクエストについてパスと処理時間をログに出力する。
func Logging() Middleware {
// 新しい Middleware を作る。
return func(f http.HandlerFunc) http.HandlerFunc {
// http.HandlerFunc を定義する。
return func(w http.ResponseWriter, r *http.Request) {
// ミドルウェアとしての処理を行う。
start := time.Now()
defer func() { log.Println(r.URL.Path, time.Since(start)) }()
// チェインの次のミドルウェア/HTTP ハンドラを呼び出す。
f(w, r)
}
}
}
// Method
// 特定の HTTP メソッドから呼ばれたことを保証する。それ以外の場合は 400 Bad Request を返す。
func Method(m string) Middleware {
// 新しい Middleware を作る。
return func(f http.HandlerFunc) http.HandlerFunc {
// http.HandlerFunc を定義する。
return func(w http.ResponseWriter, r *http.Request) {
// ミドルウェアとしての処理を行う。
if r.Method != m {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// チェインの次のミドルウェア/HTTP ハンドラを呼び出す。
f(w, r)
}
}
}
// Chain
// http.HandlerFunc に対し全てのミドルウェアを適用する。
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
http.ListenAndServe(":8080", nil)
}
$ go run advanced-middleware.go
2017/02/11 00:34:53 / 0s
$ curl -s http://localhost:8080/
hello world
$ curl -s -XPOST http://localhost:8080/
Bad Request
補足
なかなか難しいですね。この例のキモになる部分がどのように動作するか見ていきましょう。
Logging
と Method
によってミドルウェアを二つつくり Chain
によって Hello
とくっつけています。
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
と
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
の部分を展開してみましょう。すると次のようになります。
// (1) Chain に渡す引数の生成
// 引数として渡された http.HandlerFunc を実行し、
// その実行時間をログに出力する http.HandlerFunc を生成するミドルウェア関数を生成する。
loggingMiddleware := Logging()
// HTTP メソッドが GET であれば引数として渡された http.HandlerFunc を実行する
// http.HandlerFunc を生成するミドルウェア関数を生成する。
methodMiddleware := Method("GET")
// レスポンスとして 'hello world' を出力する http.HandlerFunc 。
hello := Hello
// (2) Chain の内部
// HTTP メソッドが GET であればレスポンスとして 'hello world' を出力する
// http.HandlerFunc を生成する。
helloWithMethod := methodMiddleware(hello)
// HTTP メソッドが GET であればレスポンスとして 'hello world' を出力し、
// その実行時間をログに出力する http.HandlerFunc を生成する。
helloWithMethodAndLogging := loggingMiddleware(helloWithMethod)
// (3) Chain の実行後
// 二つのミドルウェアが適用された http.HandlerFunc をサーバーに登録する。
http.HandleFunc("/", helloWithMethodAndLogging)
createMiddleware
の構造と Chain
を使うことによって任意の数のミドルウェアをつなげることができます。またミドルウェア関数を直接記述するのではなく、ミドルウェアを生成する関数を使うことによってクロージャにして状態を持たせたり、Method
のように引数によって動作をカスタマイズすることができます。この設計については Writing middleware in #golang and how Go makes it so much fun. を参照してください。