🎯 什麼情境該想到我
當你「想在既有行為外面加一層(log、重試、快取、限流、計時),而且想自由疊加」的時候。
★ Go 的 middleware 就是這個模式——這是 Go 生態裡最常用、也最值得徹底理解的一個模式。
⚙️ 怎麼用
意圖
動態地給物件加上額外的職責。就擴充功能而言,裝飾者提供了比繼承更有彈性的替代方案。
Component(介面)
▲
├── ConcreteComponent(核心邏輯)
└── Decorator ──持有──▶ Component ★ 實作同一個介面,又持有同一個介面
├── LoggingDecorator
├── RetryDecorator
└── CacheDecorator
★ 關鍵:裝飾者「實作」的介面和它「持有」的介面是同一個。
這就是為什麼可以無限層層包裹。
★ Go 的長相:middleware
// 核心介面
type Handler interface {
Handle(ctx context.Context, req Request) (Response, error)
}
// ★ Middleware = 「拿一個 Handler,回傳一個 Handler」
type Middleware func(Handler) Handler
func WithLogging(logger *slog.Logger) Middleware {
return func(next Handler) Handler {
return HandlerFunc(func(ctx context.Context, req Request) (Response, error) {
start := time.Now()
resp, err := next.Handle(ctx, req) // ★ 呼叫被包裝的那一層
logger.Info("handled", "dur", time.Since(start), "err", err)
return resp, err
})
}
}
func WithRetry(n int) Middleware {
return func(next Handler) Handler {
return HandlerFunc(func(ctx context.Context, req Request) (Response, error) {
var err error
for i := 0; i < n; i++ {
var resp Response
if resp, err = next.Handle(ctx, req); err == nil {
return resp, nil
}
if !isRetryable(err) { break } // ★ 永久性錯誤不重試
select {
case <-time.After(backoff(i)):
case <-ctx.Done():
return Response{}, ctx.Err()
}
}
return Response{}, err
})
}
}
// ★ 組裝:Chain 讓疊加順序清楚可讀
func Chain(h Handler, ms ...Middleware) Handler {
for i := len(ms) - 1; i >= 0; i-- { // ★ 反向套用,讓宣告順序 = 執行順序
h = ms[i](h)
}
return h
}
h := Chain(coreHandler,
WithLogging(logger), // 最外層 —— 最先執行、最後結束
WithMetrics(),
WithRetry(3),
WithTimeout(5*time.Second),
) // 最內層 —— coreHandlernet/http 版本(幾乎每個 Go Web 專案都有這段):
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("req", "path", r.URL.Path, "dur", time.Since(start))
})
}
mux = Logging(Auth(RateLimit(mux)))★ 這裡用到了 配接器模式 的 http.HandlerFunc —— 兩個模式天然搭在一起。
★ 順序有意義(最常被忽略的一點)
WithLogging(WithRetry(core))
→ log 記錄「整個重試過程」的一次結果 ★ 看不到中間的失敗
WithRetry(WithLogging(core))
→ 每次重試都會被 log 記錄 ★ 看得到每次嘗試
通用的順序原則(由外到內):
Recover(panic 攔截) ← 必須最外層,才能接住所有人的 panic
Logging / Tracing
Metrics
Auth / RateLimit ← 擋掉的請求不該浪費後面的資源
Timeout
Retry ← ★ 要在 Timeout 內層,否則重試會超出總時限
核心邏輯
標準庫裡的 Decorator
| 案例 | 加了什麼 |
|---|---|
bufio.NewWriter(w) | 緩衝 |
gzip.NewWriter(w) | 壓縮 |
io.LimitReader(r, n) | 讀取上限 |
io.TeeReader(r, w) | 讀的同時寫一份 |
httputil.NewSingleHostReverseProxy | 轉發(也是 代理模式) |
★ io.Reader / io.Writer 這套可無限組合的介面,是 Go 標準庫最成功的設計:
gzip.NewWriter(bufio.NewWriter(f)) // 壓縮 → 緩衝 → 檔案TypeScript 的長相
type Handler = (req: Request) => Promise<Response>;
type Middleware = (next: Handler) => Handler;
const withLogging: Middleware = (next) => async (req) => {
const t = Date.now();
try { return await next(req); }
finally { console.log(req.url, Date.now() - t); }
};
const chain = (h: Handler, ...ms: Middleware[]) =>
ms.reduceRight((acc, m) => m(acc), h);★ Express/Koa/Hono 的 middleware 全是這個模式。
TS 版更簡潔,因為Handler直接就是函式型別,連 adapter 都不用。
(Express 的(req, res, next)是變形——用next()顯式傳遞而非回傳包裝後的函式。)
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 除錯會變難。堆疊追蹤裡多了很多層匿名函式,panic 的位置不直覺。
→ 對策:middleware 命名清楚、在 log 裡帶 trace id(見 工具-遙測與監控)。 - ★ 順序錯了會很難發現。Retry 包在 Timeout 外面 → 總時間變成
n × timeout,SLO 直接破。
→ 把順序寫成註解或常數,並且測試它(見上方的順序原則)。 - 不要把業務邏輯放進 middleware。它該處理的是橫切關注點(log、認證、重試、限流)。
業務判斷放在 middleware 裡會讓流程無法追蹤。 - ★ 每一層都有成本。十層 middleware 在高 QPS 下是實際的開銷(函式呼叫 + 可能的配置)。
熱路徑上要量。 - 狀態要小心。Decorator 通常應該是無狀態的;需要狀態(如 rate limiter 的計數)時要注意並行安全。
- 和 代理模式 的界線:兩者結構幾乎相同。
Decorator 是「加功能」(可疊加、使用者主動選);Proxy 是「控制存取」(通常一層、對使用者透明)。
實務上這條線很模糊,不要為了分類糾結。 - ★ 和繼承比:三個可選行為用繼承需要 個子類(類別爆炸);用 Decorator 只要 3 個。
這就是 工具-優先組合而非繼承 的具體展現。