kikki's tech note

技術ブログです。UnityやSpine、MS、Javaなど技術色々について解説しています。

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を作る」でした。

*1:プロジェクトパスには、【ホスティング名】/【ユーザ名】/【プロジェクト名】 などを指定する


※無断転載禁止 Copyright (C) kikkisnrdec All Rights Reserved.