Go
golang
Web
go-web-examples

Go Web Examplesの和訳(Middleware: Advanced)

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


補足

なかなか難しいですね。この例のキモになる部分がどのように動作するか見ていきましょう。

LoggingMethod によってミドルウェアを二つつくり 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. を参照してください。