🎯 什麼情境該想到我

當你「想臨時把訊息導去經過額外的驗證、測試或除錯步驟,之後又能切回正常路徑」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:用一個可由控制匯流排切換的路由,讓訊息在需要時繞道經過額外處理(驗證 / 測試 / 除錯),平時則走直達路徑。

做法 / 結構要點:

  • 在路徑上放一個可切換的路由器(受控制匯流排控制)。
  • 開關開啟時,訊息被導去經過額外的中間步驟再回到主流程。
  • 關閉時訊息直達,不受影響。

★★ 這個模式的真正價值:它讓「觀察」不影響「正確性」

正常:  producer ──▶ [主通道] ──▶ consumer
繞道:  producer ──▶ [驗證/記錄/轉換] ──▶ [主通道] ──▶ consumer
                         ↑ ★ 可以隨時插入、隨時移除,不必改兩端

★ 和 竊聽器 的關鍵差別:

★ 竊聽器:複製一份出去(★ 主流程不受影響,是「旁觀」)
★ 繞道  :★★ 訊息真的改走另一條路(★ 主流程被改變,是「介入」)

→ 所以繞道能做竊聽器做不到的事(阻擋、延遲、修改、重排),
代價是它在主路徑上,它掛了訊息就停了。

★ 四個真實用途

用途說明
① ★ 除錯期的檢查點把訊息導去一個會詳細記錄的元件,確認內容正確後再放行
② ★★ 遷移期的雙寫驗證導到新舊兩套實作,★ 比對結果是否一致,不一致就告警——新系統不上線就能驗證
③ ★ 灰度/金絲雀1% 的訊息繞道到新版消費者
④ ★★ 取代「過濾器」的丟棄不符條件的訊息不要丟掉,導去一條 filtered.* 通道(見 訊息過濾器)

★★ ② 是最有價值的一個,而且常被忽略:

遷移訂單處理邏輯 → 舊系統照常處理(真的生效)
                → 同時把訊息繞道給新系統(★ 只計算,不產生副作用)
                → ★★ 比對兩者的輸出,累積幾週的一致性資料
→ ★ 這讓「換掉核心邏輯」變成可以量化風險的事

★ 這其實就是 工具-特徵測試 的線上版本,也和 A-B測試與假設驅動開發 同源。

★★ Go 的實作要點:繞道必須可以「零風險關掉」

// ★ 繞道一定要是可動態開關的 —— 否則它就是一個永久的額外風險
type Detour struct {
    enabled atomic.Bool       // ★ 由 [[控制匯流排]] 或 feature flag 控制
    shadow  Sink              // 影子目的地
    main    Sink
}
 
func (d *Detour) Handle(ctx context.Context, msg Message) error {
    if d.enabled.Load() {
        // ★★ 影子路徑的失敗絕對不能影響主路徑
        go func() {
            sctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
            defer cancel()
            if err := d.shadow.Send(sctx, msg); err != nil {
                metrics.ShadowErrors.Inc()      // ★ 只記錄,不回傳錯誤
            }
        }()
    }
    return d.main.Send(ctx, msg)               // ★ 主路徑的行為完全不變
}

★★ 三個關鍵:

  1. 影子路徑的失敗只記錄,不回傳錯誤 —— 否則繞道會讓你的系統變得更不可靠。
  2. ★ context.WithoutCancel(Go 1.21+)—— 主路徑完成後 ctx 被取消,
    影子路徑不該跟著被砍掉。
  3. 獨立的超時 —— 影子系統慢不該拖慢主路徑。

⚠️ 但 go func() 的版本有一個代價:影子路徑「可能漏訊息」(行程重啟時)。
★ 需要完整比對時,要改成「同步發到一條真的影子通道」,讓它有自己的可靠性保證。

★★ 最重要的警告:繞道要有退場日期

「我們先加一個繞道來觀察」
→ ★ 半年後它還在,而且:
  ① 主路徑上多一跳(延遲、額外的失敗模式)
  ② ★★ 沒人記得它為什麼存在,也沒人敢拿掉
  ③ ★ 它產生的資料沒人看(★ 比不存在更糟——你以為有在監控)
→ ★ 對策:加繞道時就開一張帶到期日的 ticket

★ 兄弟辨析

差別
竊聽器★ 只複製,不介入主流程;★ 更安全但能做的事更少
訊息過濾器★ 丟掉訊息;繞道是「導去別處」——★ 通常繞道是更好的選擇
智慧代理★ 攔在中間並記錄請求-回覆的配對
訊息歷史記錄走過的路徑,不改變路徑
工具-特徵測試★ 雙寫驗證是它的線上版本

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 繞道邏輯常駐但多半關閉,要確保關閉時開銷極低。

  • 切換要能動態、即時(透過控制匯流排)。

  • 繞道步驟本身別改壞訊息內容(除非刻意)。

  • ★★ 繞道在主路徑上,它掛了訊息就停了。 竊聽器只是旁觀,更安全。

  • ★★ 影子路徑的失敗只能記錄,不能回傳錯誤,否則繞道讓系統變得更不可靠。

  • ★ 用 context.WithoutCancel(Go 1.21+),否則主路徑完成後影子路徑被砍掉。

  • ★★ 繞道要有退場日期。半年後沒人記得它為什麼存在、也沒人敢拿掉,而它產生的資料沒人看——比不存在更糟。

  • ★ 一定要可動態開關(feature flag/控制匯流排),否則它是一個永久的額外風險。

  • ★ go func() 版的影子路徑可能漏訊息(行程重啟時)。需要完整比對就改成真的影子通道。

  • ★ 雙寫驗證(導到新舊兩套比對結果)是這個模式最有價值的用法,常被忽略。

  • 不要用繞道做「暫時的業務邏輯」。它會變成隱形的、不在主程式碼裡的規則。

🔗 相關工具