Go

Goで始めるMiddleware

More than 1 year has passed since last update.

ディップ Advent Calendar 2017の8日目です:sushi::pizza::beer:


Middlewareとは

そもそもMiddlewareとは何かと言うと、私の認識ではアプリケーションの処理の前後で何らかの処理を行ったりするものである。例えば、セッション管理したりユーザ認証したりする(以下の図がわかりやすい)。

このような構造のため、ビジネスロジックとなるアプリケーションはMiddlewareのことを気にせずに実装できる。

onion.png

https://mattstauffer.com/blog/laravel-5.0-middleware-filter-style/#what-is-middleware より引用


なぜGoでMiddlewareを書きたいのか

なぜGoでMiddlewareを始めたいのか、そのモチベーションは何か。

おそらくだがフレームワークを使用している場合には、フレームワークがよろしくやってくれるので自分でMiddlewareをどうこうしたいというモチベーションはあまりない気がする。しかし、あくまで個人の主観だが、標準パッケージが利用しやすいGoでは、シンプルなAPIなどであればフレームワークを使用せずに標準のnet/httpを使用して書きたいケースなどは十分あり得る。そして、標準のnet/httpを使用して書く場合には自分でMiddlewareまわりを実装する必要が発生するはずである(パッケージを使用して実装するのも含め)。このため、GoにおいてはMiddlewareを書きたくなるような場合がよくあると思う。


GoでMiddlewareを書くと

では、GoでMiddlewareを書くとどうなるのか。基本的な書き方は以下の記事を参考にするとわかりやすい。

https://www.nicolasmerouze.com/middlewares-golang-best-practices-examples/

具体的にコードとして実装すると以下のようになる。


firstMiddleware.go

package main

import (
"fmt"
"net/http"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Hello, middleware!")
}

// aboutHandler ...
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("This is midlleware test!!")
}

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[START] middleware1")
next.ServeHTTP(w, r)
fmt.Println("[END] middleware1")
}
}

// middleware2 ...
// middleware3 ...

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

http.ListenAndServe(":8888", mux)
}


[START] middleware1

[START] middleware2
[START] middleware3
This is midlleware test!!
[END] middleware3
[END] middleware2
[END] middleware1

ポイントとしては、Middlewareをfunc(hf http.HandlerFunc) http.HandlerFuncの関数として扱い、入れ子のようにすることである。このように実装することで、例えば複数のMiddlewareを実装したい場合もmiddleware1(middleware2(middleware3(indexHandler)))と入れ子を繰り返すことによって簡単に実現できる。

なお、疑似コードっぽく書くと以下のようになる。



// sampleMiddleware
func sampleMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 前処理
next.ServeHTTP(w, r)
// 後処理
}
}


GoでMiddlewareをいい感じに書くと

GoでMiddlewareを始めることができた。Middlewareの追加もできた。

しかし、追加するMiddlewareがさらに増えるとめんどくさそうなのは容易にわかると思う。

ではこの場合はどう実装したらいいだろうか。解の一つとしてMiddlewareスタックのようなものを作るというのがある。


firstMiddleware.go

package main

import (
"fmt"
"net/http"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[START] middleware1")
next.ServeHTTP(w, r)
fmt.Println("[END] middleware1")
}
}

// middleware2 ...
// middleware3 ...

func main() {
middlewares := newMws(middleware1, middleware2, middleware3)

mux := http.NewServeMux()
mux.HandleFunc("/", middlewares.then(indexHandler))
mux.HandleFunc("/about", middlewares.then(aboutHandler))

http.ListenAndServe(":8888", mux)
}

type middleware func(http.HandlerFunc) http.HandlerFunc

type mwStack struct {
middlewares []middleware
}

func newMws(mws ...middleware) mwStack {
return mwStack{append([]middleware(nil), mws...)}
}

func (m mwStack) then(h http.HandlerFunc) http.HandlerFunc {
for i := range m.middlewares {
h = m.middlewares[len(m.middlewares)-1-i](h)
}
return h
}


比較するとこうなる。Middlewareの入れ子を何回も書く必要がなくなる。

// 前

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

http.ListenAndServe(":8888", mux)
}

// 後
func main() {
middlewares := newMws(middleware1, middleware2, middleware3)

mux := http.NewServeMux()
mux.HandleFunc("/", middlewares.then(indexHandler))
mux.HandleFunc("/about", middlewares.then(aboutHandler))

http.ListenAndServe(":8888", mux)
}


パッケージを使うと

ではパッケージを使うとどうだろうか。ここではAliceとnegroniを紹介する。


Alice

justinas/alice

Aliceを使用すると前述したMiddlewareスタックのようなものを容易に実装できる(というか元ネタ)。以下、Aliceの説明を引用する。


Alice provides a convenient way to chain your HTTP middleware functions and the app handler.

In short, it transforms

Middleware1(Middleware2(Middleware3(App)))

to

alice.New(Middleware1, Middleware2, Middleware3).Then(App)


コードを見ればわかるが考え方的には前述したMiddlewareスタックのようなものをalice.New(Middleware1, Middleware2, Middleware3)で作ってから.Then(App)でハンドラを返すようになっている。Aliceを使用すると実装は以下のようになる。


aliceSample.go

package main

import (
"fmt"
"net/http"

"github.com/justinas/alice"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[START] middleware1")
next.ServeHTTP(w, r)
fmt.Println("[END] middleware1")
})
}

// middleware2 ...
// middleware3 ...

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/about", aboutHandler)

chain := alice.New(middleware1, middleware2, middleware3)

http.ListenAndServe(":8888", chain.Then(mux))
}



negroni

urfave/negroni

negroniも同様にMiddlewareスタックのようなものを扱うが、これまで紹介したものと違い実装する側がそのスタックを意識しなくてよい。negroni独自にHandlerインターフェースを用意しており、これに沿って実装すれば容易にMiddlewareの追加もできる。また、デフォルトでロガーやリカバリー処理(negroni.Classic())を用意してくれており、比較的リッチなパッケージとなっている。

https://github.com/urfave/negroni#negroniclassic

negroniを使用すると実装は以下のようになる。


negroniSample.go

package main

import (
"fmt"
"net/http"

"github.com/urfave/negroni"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
fmt.Println("[START] middleware1")
next(w, r)
fmt.Println("[END] middleware1")
}

// middleware2 ...
// middleware3 ...

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/about", aboutHandler)

n := negroni.New()
n.Use(negroni.HandlerFunc(middleware1))
n.Use(negroni.HandlerFunc(middleware2))
n.Use(negroni.HandlerFunc(middleware3))
n.UseHandler(mux)
http.ListenAndServe(":8888", n)
}



まとめると

Middlewareなんてもう怖くない:sushi::pizza::beer:

とはいえまだGoを書き始めて間もないので他に良さげな書き方やパッケージがあればどしどし教えていただきたいです:bow: