LoginSignup
76
49

More than 5 years have passed since last update.

Go でアプリケーションとクライアントのミドルウェアを作成する方法知ってますか?

Posted at

世の中に沢山の「ミドルウェア」が存在しますが、ここで紹介するミドルウェアは、あるメインロジックを大きく変更することなく、その前後に挟む処理のことを指します。

アプリケーションを作成する場合に、メインロジックのハンドラを 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 両方の情報を扱える)

次にメインロジックを持つハンドラに、ミドルウェアを適用させるために下記の関数を作成します。この処理は始めに挙げた 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.DefaultTransporthttp.Transport の型を持ちます。http.Transport は、HTTP, HTTPS、および HTTP プロキシをサポートするための RoundTripper になります。

つまり、アプリケーションミドルウェアと同様に、第一引数へメインロジックを持つ RoundTripper を渡して、その後の引数に続けて RoundTripper を満たしたクライアントのミドルウェアを渡すことで実現可能となります。

Microservices が流行っている今の時代では、複数のサービスへリクエストを送るためにそれぞれに合わせたクライアントを用意する必要が出てきました。それぞれのクライアントにも共通のロジックを持たせる可能性があります。そこで今回のミドルウェアの作成に関する知見を活かせると良いですね!

76
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
49