🎯 什麼情境該想到我

當你「想在既有行為外面加一層(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),
)                            // 最內層 —— coreHandler

net/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 個。
    這就是 工具-優先組合而非繼承 的具體展現。

🔗 相關工具