本文 AI 產出,尚未審核

Golang 網路編程 – 中間件(Middleware)設計

簡介

在 Web 服務或 API 伺服器中,中間件是介於請求(Request)與最終處理函式(Handler)之間的可重用組件。它負責執行日誌、認證、授權、跨域(CORS)設定、壓縮、錯誤統一處理等共通工作,使得核心業務邏輯保持簡潔、易於測試。

對於使用 Go 標準庫 net/http 或流行的路由框架(如 gorilla/muxchigin)的開發者而言,了解 如何設計、組合與管理中間件,是提升程式碼可讀性與可維護性的關鍵。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你打造乾淨且彈性的 Middleware 鏈。


核心概念

1. Middleware 的函式簽名

在 Go 的 net/http 中,Handler 的型別是 http.Handler(具備 ServeHTTP(ResponseWriter, *Request) 方法)或更常見的 http.HandlerFunc
Middleware 本質上是一個 接受 http.Handler 並回傳 http.Handler 的高階函式

type Middleware func(http.Handler) http.Handler

這樣的設計允許我們把多個 Middleware 串成鏈(Chain),最終交給路由器或 http.Server

2. 串接 Middleware

常見的串接方式是「從左到右」或「從右到左」依序包裝。例如:

finalHandler := http.HandlerFunc(myHandler)
handlerWithMW := loggingMW(authMW(finalHandler))

authMW 先被包裝,接著 loggingMW 再包裝,請求會先通過 loggingMWauthMWmyHandler

3. 何時呼叫 next.ServeHTTP

每個 Middleware 必須決定是否 繼續傳遞 請求給下一層。若在某個階段已經產生回應(例如認證失敗),就不應再呼叫 next.ServeHTTP,以避免重複寫入回應。

4. Context 的傳遞

Go 1.7 起 *http.Request 內建 Context,是跨 Middleware 傳遞請求範圍資料的標準方式。利用 context.WithValue 可以把驗證資訊、使用者 ID 等放入 Context,後續的 Handler 或 Middleware 能安全取得。


程式碼範例

以下示範五個常見且實用的 Middleware,皆以 純標準庫 為基礎,方便直接套用於任何 Go 專案。

1️⃣ Logging Middleware(請求日誌)

package middleware

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

// LoggingMW 記錄每筆請求的 method、path、狀態碼與耗時
func LoggingMW(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// 使用自訂的 ResponseWriter 取得狀態碼
		lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
		next.ServeHTTP(lrw, r)

		duration := time.Since(start)
		log.Printf("%s %s %d %s", r.Method, r.URL.Path, lrw.statusCode, duration)
	})
}

// loggingResponseWriter 包裝 http.ResponseWriter 以捕捉狀態碼
type loggingResponseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
	lrw.statusCode = code
	lrw.ResponseWriter.WriteHeader(code)
}

重點:透過自訂 ResponseWriter 捕捉回傳的 HTTP 狀態碼,讓日誌更完整。


2️⃣ Authentication Middleware(簡易 Token 驗證)

package middleware

import (
	"context"
	"net/http"
	"strings"
)

type ctxKey string

const userIDKey ctxKey = "userID"

// AuthMW 檢查 Authorization Header,若驗證失敗直接回 401
func AuthMW(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 期待 "Bearer <token>"
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		token := strings.TrimPrefix(authHeader, "Bearer ")

		// 這裡僅示範,實務上應該呼叫 JWT 驗證或 OAuth2 服務
		userID, err := validateToken(token)
		if err != nil {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}

		// 把 userID 放入 Context,供後續使用
		ctx := context.WithValue(r.Context(), userIDKey, userID)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// 假的驗證函式,實務請換成真正的 JWT 驗證
func validateToken(tok string) (string, error) {
	if tok == "valid-token" {
		return "12345", nil
	}
	return "", fmt.Errorf("invalid")
}

// 從 Context 取得使用者 ID 的輔助函式
func UserIDFromContext(ctx context.Context) (string, bool) {
	id, ok := ctx.Value(userIDKey).(string)
	return id, ok
}

說明:使用 context.WithValue 把驗證後的使用者資訊傳遞下去,避免在每個 Handler 中重複解析 Token。


3️⃣ CORS Middleware(跨來源資源共享)

package middleware

import "net/http"

// CORSMW 允許前端從指定來源呼叫 API,支援 Preflight (OPTIONS) 請求
func CORSMW(allowedOrigins []string) Middleware {
	originMap := make(map[string]struct{}, len(allowedOrigins))
	for _, o := range allowedOrigins {
		originMap[o] = struct{}{}
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			origin := r.Header.Get("Origin")
			if _, ok := originMap[origin]; ok {
				w.Header().Set("Access-Control-Allow-Origin", origin)
				w.Header().Set("Vary", "Origin")
				w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
				w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
				w.Header().Set("Access-Control-Allow-Credentials", "true")
			}

			// 若是 Preflight,直接回 200
			if r.Method == http.MethodOptions {
				w.WriteHeader(http.StatusOK)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

技巧Vary: Origin 告訴快取代理(如 CDN)不同來源的回應不可共用,避免跨站資訊外洩。


4️⃣ Panic Recovery Middleware(防止程式崩潰)

package middleware

import (
	"log"
	"net/http"
	"runtime/debug"
)

// RecoverMW 捕捉 panic,寫入 500 回應並記錄堆疊
func RecoverMW(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("panic recovered: %v\n%s", rec, debug.Stack())
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

實務:在正式環境一定要加上此層,避免單一請求的 panic 讓整個服務掛掉。


5️⃣ Request Timeout Middleware(請求逾時)

package middleware

import (
	"context"
	"net/http"
	"time"
)

// TimeoutMW 為每筆請求設定最長執行時間,超時則回 504
func TimeoutMW(d time.Duration) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx, cancel := context.WithTimeout(r.Context(), d)
			defer cancel()

			// 建立一個 channel 讓下層 handler 通知是否完成
			done := make(chan struct{})
			go func() {
				next.ServeHTTP(w, r.WithContext(ctx))
				close(done)
			}()

			select {
			case <-ctx.Done():
				// 超時或客戶端取消
				if ctx.Err() == context.DeadlineExceeded {
					http.Error(w, "Gateway Timeout", http.StatusGatewayTimeout)
				}
			case <-done:
				// 正常完成
			}
		})
	}
}

注意:若下層 Handler 仍在執行,cancel() 會讓它的 Context 失效,開發者應在長時間操作(如 DB、外部 API)時檢查 ctx.Err()


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記呼叫 next.ServeHTTP Middleware 只寫了前置檢查,卻未把請求傳遞下去,導致 404 或無回應。 確認所有非終止條件(如認證失敗)之外,都必須呼叫 next.ServeHTTP
在回應已寫入後再次寫入 例如在 loggingMW 中先 WriteHeader,之後的 Middleware 再寫入,會產生 http: multiple response.WriteHeader calls 錯誤。 使用自訂 ResponseWriter 捕捉狀態碼,或在需要寫入前檢查 w.WriteHeader 是否已呼叫。
Context 資料洩漏 把大量資料直接放入 Context,會增加記憶體使用且違背 Context 的設計初衷。 僅放入少量、請求範圍的「元資料」(如 userID、traceID)。
過度堆疊 Middleware 每層 Middleware 都會產生額外的函式呼叫與閉包,過多層會影響效能。 只保留必要的功能,將相似功能合併(例如把 CORS 與安全 Header 合併成一層)。
在 RecoverMW 內直接寫入回應 若下層已寫入部分回應,http.Error 會失效。 使用 http.NewResponseRecorder 暫存回應,確保在 panic 時能完整控制輸出。

最佳實踐

  1. 保持單一職責:每個 Middleware 只做一件事(如日誌、認證、CORS),方便測試與重用。
  2. 使用函式型別 Middleware:統一簽名,讓組合更直觀。
  3. 在最外層放置 RecoverMW,保證所有 panic 都能被捕獲。
  4. 把共用資料放入 Context,並提供明確的取得函式(如 UserIDFromContext),避免在每個 Handler 重複解析。
  5. 測試每層 Middleware:利用 httptest.NewRecorderhttp.NewRequest 撰寫單元測試,確保前置與後置行為正確。

實際應用場景

場景 需要的 Middleware 為什麼需要
公開 API CORS、Logging、Rate‑Limit、Recover 跨域、流量監控、避免惡意攻擊、保證服務穩定
內部管理系統 Auth (Session 或 JWT)、CSRF、Logging、Recover 必須驗證使用者身分、防止跨站請求偽造、追蹤操作紀錄
微服務間呼叫 TraceID (分散式追蹤)、Timeout、Recover、Metrics 追蹤請求流向、避免單一服務卡住全鏈路、收集效能指標
檔案上傳服務 Auth、Logging、Recover、Body‑Size‑Limit 確保上傳者合法、紀錄上傳行為、防止大檔案耗盡資源
WebSocket 升級 Auth、Logging、Recover 在升級前先驗證,保持連線期間的錯誤捕獲與日誌

範例:下面示範如何在 main.go 中組合上述 Middleware,形成一條完整的處理鏈。

package main

import (
	"net/http"
	"time"

	"github.com/yourname/project/middleware"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", helloHandler)

	// 組合 Middleware(從左到右依序執行)
	handler := middleware.RecoverMW(
		middleware.LoggingMW(
			middleware.AuthMW(
				middleware.CORSMW([]string{"https://example.com", "http://localhost:3000"})(
					middleware.TimeoutMW(5 * time.Second)(
						mux,
					),
				),
			),
		),
	)

	httpServer := &http.Server{
		Addr:    ":8080",
		Handler: handler,
	}
	if err := httpServer.ListenAndServe(); err != nil {
		panic(err)
	}
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	// 取得使用者 ID(若已通過 AuthMW)
	if uid, ok := middleware.UserIDFromContext(r.Context()); ok {
		w.Write([]byte("Hello, user " + uid))
		return
	}
	w.Write([]byte("Hello, guest"))
}

在此範例中,RecoverMW 放在最外層,保證任何 panic 都會被捕獲;LoggingMW 緊接其後,確保即使發生錯誤也能寫入日誌;其餘功能則依需求排列。


總結

  • Middleware 是 Go 網路程式設計中不可或缺的組件,透過 高階函式 把共通功能抽離、重用與組合。
  • 正確的 函式簽名、Context 傳遞與 next.ServeHTTP 呼叫順序,是避免常見錯誤的關鍵。
  • 本文提供的 五個實用範例(Logging、Auth、CORS、Recover、Timeout)涵蓋了大多數實務需求,讀者可依專案需求自行擴充或合併。
  • 謹記 單一職責、最外層放 Recover、測試每層 的最佳實踐,能讓服務在面對高併發、跨域或安全挑戰時保持穩定與可維護。

掌握了 Middleware 的設計與組合,你的 Go Web 應用將會變得 更乾淨、更安全、更具彈性,也更容易在團隊中進行協作與持續交付。祝開發順利!