Golangでいい感じのMiddlewareを作る
本章では、Go言語でいい感じのMiddleware構成を検討してみたいと思います。
前置き
Middlewareは、通常、OSとAPPとの中間ソフトウェアを指しますが、ここではGolangのWebサーバーにおける、HTTPリクエストとHTTP handle関数間の中間処理を指します。
前準備
今回は、Go 1.11からサポートされた、新しいモジュール管理機構を利用しています。詳細については、以下の公式を参照ください。
github.com
簡単ではありますが、有効化の手順を紹介します。
Go Moduleの有効化
$ go mod init 【プロジェクトパス】
*1
なお、インポートしたファイルをアップデートする場合には、以下のコマンドを実行します。
$ go mod tidy
既存のMiddlewareの小さな課題
現在、色々なMiddlewareが公開されています。有名なライブラリとして、以下のライブラリがあります。 github.com github.com しかし、aliceではMiddlewareに直接実装を含む必要がありDIが行えず、negroniでは独自のHTTP handle関数を利用していて、アーキテクチャとしては少々使いづらいものがあります。
そこで今回、DIが行えつつGolangのhttpライブラリを生かせる、Middlewareの仕組みを考えてみたいと思います。
ファイル構成
ファイルの構成は以下の通りです。
(project) |-- cmd | `-- server | `-- main.go `-- pkg |-- http | `-- rest | `-- handler.go |-- middleware | |-- log.go | |-- logger_local.go | `-- stack.go `-- stacking `-- log.go
MiddlewareをStackできる仕組みづくり
まず、Middlewareを複数Stackして処理できる機構を用意します。StackさせるMiddlewareは、HTTP handle関数を定義した構造体です。
[pkg/middleware/stack.go]
package middleware import "net/http" type chainHandler func() http.Handler type chainMiddleware func(next http.Handler) http.Handler // A structure to stack middleware functions type Stack struct { chains []chainMiddleware handler chainHandler } // Execute a Http Handle Function func (s Stack) Then(h http.Handler) http.Handler { s.handler = func(_h http.Handler) chainHandler { return func() http.Handler { return _h } }(h) return s } // Initialize a Stack structure by arguments func (s Stack) New(chains ...chainMiddleware) Stack { return Stack{ chains: chains, } } // Add middleware after initialization func (s Stack) Append(chains ...chainMiddleware) Stack { newChains := make([]chainMiddleware, len(s.chains)+len(chains)) copy(newChains[:len(s.chains)], s.chains) copy(newChains[len(s.chains):], chains) s.chains = newChains return s } func (s Stack) ServeHTTP(w http.ResponseWriter, r *http.Request) { final := s.handler() for i := len(s.chains) - 1; i >= 0; i-- { final = s.chains[i](final) } final.ServeHTTP(w, r) }
Middlewareとして利用するログ機構の準備
今回は、Middlewareに採用するサンプルとしてログ機構を準備してみます。まずは、インタフェースを定義します。
[pkg/middleware/log.go]
package middleware import "net/http" type Log interface { Info(interface{}) Warn(interface{}) Error(interface{}) }
続いて、ログ機構の本体を定義します。今回はローカル環境を実装します。
[pkg/middleware/logger_local.go]
package middleware import ( "context" "fmt" "log" "net/http" "os" ) func NewLogger() (l *logWrapper, e error) { infoLog := log.New(os.Stdout, "[INFO ]", log.Llongfile|log.LstdFlags) warnLog := log.New(os.Stdout, "[WARN ]", log.Llongfile|log.LstdFlags) errorLog := log.New(os.Stdout, "[ERROR]", log.Llongfile|log.LstdFlags) return &logWrapper{ info: infoLog, warn: warnLog, error: errorLog, }, nil } type logWrapper struct { info *log.Logger warn *log.Logger error *log.Logger } func (l *logWrapper) Info(s interface{}) { l.info.Print(s) } func (l *logWrapper) Warn(s interface{}) { l.warn.Print(s) } func (l *logWrapper) Error(s interface{}) { l.error.Print(s) }
Middleware内でログの収集
前節で定義した、ログ機構をMiddlware内に実装し、ログを収集します。
[pkg/stacking/log.go]
package stacking import ( "【ホスティング名】/【ユーザ名】/【プロジェクト名】/pkg/middleware" "net/http" ) type Log struct { // Inject object Logger middleware.Log } func (s *Log) HandlerFunc(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.Logger.Info("[START]LOG") next.ServeHTTP(w, r) s.Logger.Info("[END]LOG") }) }
ルーティングの設定
そして、クライアントからのWebリクエストに対するルーティングの定義を行います。今回は、ルーティングに以下のライブラリを使用しています。
github.com
[pkg/http/rest/handler.go]
package rest import ( "github.com/bmizerany/pat" "【ホスティング名】/【ユーザ名】/【プロジェクト名】/pkg/middleware" "【ホスティング名】/【ユーザ名】/【プロジェクト名】/pkg/stacking" "net/http" ) func Handler(logger middleware.Log) http.Handler { router := pat.New() stack := middleware.Stack{} stack = stack.New( (&stacking.Log{Logger: logger}).HandlerFunc, ) router.Get("/", stack.Then(【ここに通常のHTTP handle関数を記載する】)) return router }
Webサーバーの起動
最後に、main関数でWebサーバーを起動させます。
[cmd/server/main.go]
package main import ( "fmt" "【ホスティング名】/【ユーザ名】/【プロジェクト名】/pkg/http/rest" "【ホスティング名】/【ユーザ名】/【プロジェクト名】/pkg/middleware" "log" "net/http" "os" ) func main() { port := os.Getenv("PORT") if len(port) == 0 { port = "8080" } log.Printf("Listening on server port: %s", port) logger, err := middleware.NewLogger() if err != nil { log.Fatal(err) } // set up the HTTP server router := rest.Handler(logger) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), router)) }
筆休め
クライアントやWEBに関わらず、開発では将来に渡ってのコストの低減や保守性の向上のために、フレームワークの選定や導入を行います。
フレームワークは世にたくさんありますが、自前で保守運用を続けるか、既存の仕組みの保守運用に期待するかで、眼前のコストだけでなく将来に渡ってのコストが大きく変わってきます。
なるべくシンプルで効果の高い方法を取り続けていきたいですね。
以上、「Golangでいい感じのMiddlewareを作る」でした。