Golang 網路編程 – 中間件(Middleware)設計
簡介
在 Web 服務或 API 伺服器中,中間件是介於請求(Request)與最終處理函式(Handler)之間的可重用組件。它負責執行日誌、認證、授權、跨域(CORS)設定、壓縮、錯誤統一處理等共通工作,使得核心業務邏輯保持簡潔、易於測試。
對於使用 Go 標準庫 net/http 或流行的路由框架(如 gorilla/mux、chi、gin)的開發者而言,了解 如何設計、組合與管理中間件,是提升程式碼可讀性與可維護性的關鍵。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你打造乾淨且彈性的 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 再包裝,請求會先通過 loggingMW → authMW → myHandler。
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 時能完整控制輸出。 |
最佳實踐
- 保持單一職責:每個 Middleware 只做一件事(如日誌、認證、CORS),方便測試與重用。
- 使用函式型別
Middleware:統一簽名,讓組合更直觀。 - 在最外層放置
RecoverMW,保證所有 panic 都能被捕獲。 - 把共用資料放入
Context,並提供明確的取得函式(如UserIDFromContext),避免在每個 Handler 重複解析。 - 測試每層 Middleware:利用
httptest.NewRecorder與http.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 應用將會變得 更乾淨、更安全、更具彈性,也更容易在團隊中進行協作與持續交付。祝開發順利!