📑

【Go】net/httpパッケージで使えるミドルウェアの作成例

に公開

概要

ミドルウェアの仕組みを確認するために、net/httpパッケージを使ってサンプルを作成したので、以下にその実装例を記載します。

ミドルウェアの作成例

使用するサーバのサンプル

次のサンプル実装では、/homeというエンドポイントに対してリクエストを送信すると、 {"message":"Hello World","status":"OK"}が返されます。
このサーバに対してミドルウェアを適用する例を考えます。

package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type Response struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	response := Response{
		Message: "Hello World",
		Status:  "OK",
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(response); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
		return
	}
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /home", homeHandler)

	log.Println("Server starting on :8080...")

	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

ミドルウェアが備える機能

作成するミドルウェアはつぎのことができるようにします。

  • 複数のミドルウェアを適用可能

サンプルミドルウェア

次の関数は、複数のミドルウェアを適用するための関数です。引数で受け取った可変長のミドルウェアを順番に適用します。

type Middleware func(http.HandlerFunc) http.HandlerFunc

func ApplyMiddlewares(middlewares ...Middleware) func(http.HandlerFunc) http.HandlerFunc {
	return func(handler http.HandlerFunc) http.HandlerFunc {
		for i := len(middlewares) - 1; i >= 0; i-- {
			handler = middlewares[i](handler)
		}
		return handler
	}
}

ミドルウェアは次のようになります。

func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)

		next(w, r)

		log.Printf("Request processed in %v", time.Since(start))
	}
}

func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		log.Println("Check Auth")

		apiKey := r.Header.Get("X-API-Key")
		if apiKey == "" {
			log.Println("Auth failed: missing API key")
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		log.Println("Auth passed")

		next(w, r)
	}
}

ミドルウェアを適用したサーバ

mux.HandleFuncに対して、ApplyMiddlewaresを適用しています。
この例では、「LoggingMiddleware → AuthMiddleware → homeHandler」の順番にミドルウェアとハンドラが実行されます。

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /home", ApplyMiddlewares(
		LoggingMiddleware,
		AuthMiddleware,
	)(homeHandler))

	log.Println("Server starting on :8080...")

	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

実行イメージ

mux.HandleFuncの第二引数にApplyMiddlewaresを用いている理由

サーバ起動時に、mux.HandleFuncに渡される第二引数は、つぎのようなイメージでラップされた関数です。

ラップされたHandlerFunc

ServerMux.HandleFuncの第二引数のhandlerの型と、httpパッケージのHandlerFuncのシグネチャはつぎのようになっています。

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

type HandlerFunc func(ResponseWriter, *Request)

一方で、本記事で作成したApplyMiddlewaresは、関数を返す関数となっています。

func ApplyMiddlewares(middlewares ...Middleware) func(http.HandlerFunc) http.HandlerFunc

ApplyMiddlewaresが実行されたときに返ってきた関数を、更に(homeHandler)という形で実行しており、その結果はhttp.HandlerFuncとなるため、ServerMux.HandleFuncの第二引数と一致するため、ApplyMiddlewaresを使用した形で記述することができています。

ラップされたミドルウェアが順次実行されている理由

各ミドルウェアは next(w, r) を実行するようになっているため、自身の処理を実行しつつ、ラップされているミドルウェアを呼び出すようになっています。
また、ミドルウェアは最終的にハンドラーを呼び出しています。このため、nextの前の処理は、ハンドラがリクエストを処理する前、nextの後の処理ではハンドラがリクエストを処理した後、に対応することになります。

ミドルウェアのグループ化

つぎのwithAuthのようにミドルウェアをグループ化して再利用することもできます。

func main() {
	mux := http.NewServeMux()

	withAuth := ApplyMiddlewares(LoggingMiddleware, AuthMiddleware)

	mux.HandleFunc("GET /status", withAuth(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	}))
}

ミドルウェアを適用したコードの全体像

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"time"
)

type Response struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

type Middleware func(http.HandlerFunc) http.HandlerFunc

func ApplyMiddlewares(middlewares ...Middleware) func(http.HandlerFunc) http.HandlerFunc {
	return func(handler http.HandlerFunc) http.HandlerFunc {
		for i := len(middlewares) - 1; i >= 0; i-- {
			handler = middlewares[i](handler)
		}
		return handler
	}
}

func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)

		next(w, r)

		log.Printf("Request processed in %v", time.Since(start))
	}
}

func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		log.Println("Check Auth")

		apiKey := r.Header.Get("X-API-Key")
		if apiKey == "" {
			log.Println("Auth failed: missing API key")
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		log.Println("Auth passed")

		next(w, r)
	}
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	response := Response{
		Message: "Hello World",
		Status:  "OK",
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(response); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
		return
	}
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /home", ApplyMiddlewares(
		LoggingMiddleware,
		AuthMiddleware,
	)(homeHandler))

	log.Println("Server starting on :8080...")

	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

Discussion