【Go入門】Goで自作フレームワークを作りたい③ ~ミドルウェア対応編~

# 本記事で書くこと
※本記事は[こちら](https://qiita.com/k_tsurumaki/items/c2b640514e7c3e4b4200)の記事の続きです。
前回作成したフレームワークにミドルウェアを対応させる手順とについて記載します。

# ミドルウェアとはそもそも何なのか

![fw-sample-middleware.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3933650/b8eec6c6-39d5-4a64-8494-1bde144ddcbd.png)


ミドルウェアとは、**HTTPリクエストの前後で特定の処理を行う仕組み**のことです。

どのリクエストに対しても共通で実行する処理を切り出すことでコードの重複をなくし、シンプルにすることが可能です。また一度ミドルウェアのハンドラを作成しておくことで、再利用したりミドルウェア同士を組み合わせて使うこともできます。

ミドルウェアが用いられる例としては以下のようなケースがあります。
- ログ記録
- 認証
- CORS
- バリデーション

# ミドルウェア対応手順
現状のフレームワークにミドルウェアを対応させる主な手順は以下の通りです。
1. ミドルウェアの定義
1. ミドルウェアを保持するフィールドの追加
1. ミドルウェアを登録するメソッドの追加
1. ミドルウェアを実行するように`ServeHTTP`を修正


## 1. ミドルウェアを定義
始めに、ミドルウェアを定義します。
以下のように引数・戻り値ともに`http.Handler`であるメソッドを`Middleware`として定義します。
```fw-sample.go
type Middleware func(http.Handler) http.Handler
```
引数の`Handler`はこのミドルウェアの実行後に実行されるハンドラです。
これを受け取って、その前（後）にミドルウェアとして実行する処理を追加したメソッドを返します。

```go
func examppleMildleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func (r http.ResponseWriter, req *http.Request) {
		// 何らかの処理（例：ログの記録、認証）
		fmt.Println("Middleware executed")
		// 次のハンドラを呼び出す
		next.ServeHTTP(r, req)
	})
}
```


## 2. ミドルウェアを保持するフィールドの追加
次に、`App`構造体にミドルウェアを保持するフィールドを追加します。
```fw-sample.go
// アプリケーション全体を管理する構造体
type App struct {
	Router RouterInterface
    // 追加：ミドルウェアを保持するリストを追加
	Middleware []Middleware
}
```
`Middleware`にリストとしてミドルウェアのメソッドを保持します。
GETやPOSTメソッドが呼ばれたときに、このフィールドで保持しているミドルウェアを実行することで処理を挟み込みます。

## 3. ミドルウェアを登録するメソッドの追加
2.で追加したフィールドに実際にミドルウェアを登録するメソッドを追加します。

```fw-sample.go
func (a *App) Use(m Middleware) {
	a.Middleware = append(a.Middleware, m)
}
```
ユーザはこのメソッドを使って追加したいミドルウェアを自由に追加できます。

## 4. ミドルウェアを実行するように`ServeHTTP`を修正
最後に、GETやPOSTメソッドが呼ばれたときに、`Middleware`フィールドに保持しているミドルウェアを実行するように`ServeHTTP`を修正します。
```fw-sample.go
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// ミドルウェアを適用
	// 最終的にRouterのServeHTTPを呼び出すハンドラ
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
		a.Router.ServeHTTP(w, r)
	})

	// ミドルウェアを逆順に適用
	for i := len(a.Middleware) - 1; i >= 0; i-- {
		handler = http.HandlerFunc(a.Middleware[i](handler).ServeHTTP)
	}

	// 最終的なハンドラを実行
	handler.ServeHTTP(w, r)
}
```
# 挙動確認
実際にミドルウェアを追加して正しく動作するか確認してみます。

他のプロジェクトでサーバを立ててみます。
サーバを立てる手順に関しては[こちら](https://qiita.com/k_tsurumaki/items/4ff9596587105b6413c2#%E6%8C%99%E5%8B%95%E7%A2%BA%E8%AA%8D)をご確認ください。

```server.go
package main

import (
	"log"
	"net/http"

	fwsample "github.com/k-tsurumaki/fw-sample"
)

func main() {
	app := fwsample.New()

	// ログを出力するミドルウェア
	app.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			log.Printf("Request: %s %s", r.Method, r.URL.Path)
			next.ServeHTTP(w, r)
		})
	})

	// 認証を行うミドルウェア
	app.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Header.Get("Authorization") != "my-token" {
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	})

	// ルートの登録
	// curl -X GET http://localhost:8080/hello
	// curl -X GET http://localhost:8080/hello -H "Authorization: my-token"
	app.Router.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})


	// curl -X POST http://localhost:8080/echo -d "Hello, Echo!"
	// curl -X POST http://localhost:8080/echo -H "Authorization: my-token" -d "Hello, Echo!"
	app.Router.Post("/echo", func(w http.ResponseWriter, r *http.Request) {
		body := make([]byte, r.ContentLength)
		r.Body.Read(body)
		w.Write(body)
	})

	// サーバーの起動
	log.Println("Starting server on :8080")
	if err := app.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}
```
今回は**ログ出力を行うミドルウェア**と`Authorization`ヘッダを用いた**簡単な認証を行うミドルウェア**を追加しました。

まず、`Authorization`ヘッダを設定せずにGETリクエストを投げてみます。
```bash
$curl -X GET http://localhost:8080/hello
```

以下のような回答が返ってきました。
`Authorization`ヘッダを設定していないため、認証に失敗しました。
```bash
Unauthorized
```

サーバ側ではログが出力されました。
```bash
2025/05/04 18:10:32 Request: GET /hello
```

続いて、`Authorization`ヘッダを設定してGETリクエストを投げてみます。
```bash
$curl -X GET http://localhost:8080/hello -H "Authorization: my-token"
```

認証が成功し、期待通りの回答が返ってきました。
```bash
Hello, World!
```

次回はパスパラメータ対応編！