世の中に沢山の「ミドルウェア」が存在しますが、ここで紹介するミドルウェアは、あるメインロジックを大きく変更することなく、その前後に挟む処理のことを指します。
アプリケーションを作成する場合に、メインロジックのハンドラを mainHandler
として、ミドルウェア A, B, C を使用していた場合の挙動は以下の順序の通りになります。
request -> A -> B -> C -> mainHandler -> C -> B -> A -> response
これを意識するためには、Perl Monger にお馴染みの図を覚えるといいでしょう。玉ねぎ内部のそれぞれの層はミドルウェアを表現しています。
http://blog.nomadscafe.jp/2012/01/plackmiddlewareaccesslog.html
これを踏まえて Go でまずは、アプリケーションのミドルウェアを作成してみましょう。
アプリケーションミドルウェアを作成する
下記がアプリケーションサーバーのミドルウェアになります。ここでは app.ServeHTTP
がアプリケーションのメインロジックとなり、その前後に何かしら処理を行う関数やメソッドを作成することが可能です。
type Middleware func(http.Handler) http.Handler
func Something() Middleware {
return func(app http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 前処理
Before()
app.ServeHTTP(w, r)
// 後処理
After()
})
}
}
ここで前処理、後処理を意図させる関数を記述していますが、例としてこれらが行える処理はそれぞれ以下があるでしょう。
- 前処理(この段階では唯一 request の情報を扱える)
- 受け取った request の情報をロギングする。
- もし、これから行う処理が
panic()
する場合に備えてrecover()
を挟んでおく
- 後処理(この段階では request, response 両方の情報を扱える)
- 前処理の方で
http.ResponseWriter
を struct でラップして、後処理時に書き込んだ response の情報をロギングする
- 前処理の方で
次にメインロジックを持つハンドラに、ミドルウェアを適用させるために下記の関数を作成します。この処理は始めに挙げた middleware の処理順序を保証するためのものです。
request -> A -> B -> C -> mainHandler -> C -> B -> A -> response
func UseMiddlewares(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
これらを踏まえて使い方は次の通りになります。
type Mux struct {
mux *http.ServeMux
}
// ハンドラを登録する際にミドルウェアを挟む
func (m *Mux) Handle(pattern string, handler http.Handler) {
mux.Handle(pattern, UseMiddlewares(handler,
Something(),
A(),
B(),
C(),
))
}
func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.mux.ServeHTTP(w, r)
}
func mainHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, main logic")
w.WriteHeader(http.StatusOK)
})
}
// ハンドラを登録してサーブする
func RegisterHandlersAndServe() {
server := new(http.Server)
mux := &Mux{mux: http.NewServeMux()}
mux.Handle("/hello", mainHandler())
server.Handler = mux
server.ListenAndServe()
}
Go だと標準パッケージだけで、とても簡単にミドルウェアが実装できます。
ここまで紹介したミドルウェアの作成のノウハウは、アプリケーションのミドルウェアを作るためだけではなく、実は http クライアントのミドルウェアを作成する時にも役に立ちます。
クライアントのミドルウェアを作成する
同様にクライアントのためのミドルウェアを作成してみます。 net/http
パッケージで提供されている http.RoundTripper
interface を使用します。定義は下記のようになっています。
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
RoundTripper
は単一の HTTP トランザクションを扱うためのメインロジックを持ちます。そのため RoundTripper
を実装する際に様々な注意点が存在します。
これらは GoDoc に記載されていますが、一応幾つか挙げると
- Goroutine を用いた場合に concurrent safe になるように実装する
- response を得られた場合は err == nil を返す
- request の中身を弄ってはいけない
などです。気になる方は GoDoc を読んでください。
しかし、この RoundTripper
を用いてクライアントのミドルウェアを作成することが可能です。まずは http.Handler
と同様に以下を定義します。
// Middleware represents http client middleware
type Middleware func(http.RoundTripper) http.RoundTripper
// UseMiddlewares uses http client middlewares
func UseMiddlewares(r http.RoundTripper, middlewares ...Middleware) http.RoundTripper {
for i := len(middlewares) - 1; i >= 0; i-- {
r = middlewares[i](r)
}
return r
}
次に http.HandlerFunc
に値する http.RoundTripper
の型を定義してあげます。
// RoundTripperFunc represents http.RoundTripper
type RoundTripperFunc func(*http.Request) (*http.Response, error)
// RoundTrip do RoundTrip
func (f RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}
これらを定義しておくことによって、こんな感じに簡単に http.RoundTripper
interface を満たした関数を定義することが可能になります。
func Something() Middleware {
return func(next http.RoundTripper) http.RoundTripper {
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
// 前処理
Before()
resp, err := next.RoundTrip(r)
if err != nil {
return nil, err
}
// 後処理
After()
return resp, nil
})
}
}
これらを用いて request と response のログを吐くようなミドルウェアを作成してみます。今回は、ロギングを行うパッケージとして有名な go.uber.org/zap を使用します。記述した Go のコードは以下の通りになりました。
// RequestLogging logs request contents.
func RequestLogging(logger *zap.Logger) Middleware {
return func(next http.RoundTripper) http.RoundTripper {
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
logger.Info("request logging",
zap.String("RemoteAddr", r.RemoteAddr),
zap.String("Content-Type", r.Header.Get("Content-Type")),
zap.String("Path", r.URL.Path),
zap.String("Query", r.URL.RawQuery),
zap.String("Method", r.Method),
)
return next.RoundTrip(r)
})
}
}
// ResponseLogging logs response contents.
func ResponseLogging(logger *zap.Logger) Middleware {
return func(next http.RoundTripper) http.RoundTripper {
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
resp, err := next.RoundTrip(r)
if err != nil {
return nil, err
}
logger.Info("response logging",
zap.Int("StatusCode", resp.StatusCode),
zap.Int64("ContentLength", resp.ContentLength),
zap.String("Path", r.URL.Path),
zap.String("Query", r.URL.RawQuery),
zap.String("Method", r.Method),
)
return resp, nil
})
}
}
ミドルウェアを幾つか定義したら *http.Client
を作成する関数を定義します。これを用いて http の client を作成する事ができます。
func NewClient(logger *zap.Logger) *http.Client {
return &http.Client{
Transport: UseMiddlewares(
http.DefaultTransport,
RequestLogging(logger),
ResponseLogging(logger),
),
}
}
ちなみに http.DefaultTransport
は http.Transport
の型を持ちます。http.Transport
は、HTTP, HTTPS、および HTTP プロキシをサポートするための RoundTripper
になります。
つまり、アプリケーションミドルウェアと同様に、第一引数へメインロジックを持つ RoundTripper
を渡して、その後の引数に続けて RoundTripper
を満たしたクライアントのミドルウェアを渡すことで実現可能となります。
Microservices が流行っている今の時代では、複数のサービスへリクエストを送るためにそれぞれに合わせたクライアントを用意する必要が出てきました。それぞれのクライアントにも共通のロジックを持たせる可能性があります。そこで今回のミドルウェアの作成に関する知見を活かせると良いですね!