🎯 什麼情境該想到我

當你「要把一則訊息,送給一份動態算出的收件人通道清單」的時候。

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

意圖:把訊息路由給收件人清單上的所有通道。

做法要點:

  • 先依規則(或訊息內容)計算出這則訊息該送給哪些收件人。
  • 再把訊息複製、送往清單上每一個通道。
  • 像內容路由器,但可以有多個目的地。
  • 常用於審批、比價等「要同時問多個對象」的情境。

★★ 和發布訂閱的界線:這是事件驅動架構最容易失守的一條線

// ★ 收件人清單:發送者知道收件者是誰
recipients := computeRecipients(order)         // ★ 發送者算出清單
for _, r := range recipients { publish(ctx, r, msg) }
 
// ★ 發布訂閱:發送者不知道也不在乎
publish(ctx, "orders.order.placed.v1", msg)    // ★ 誰要收是訂閱者的事

★★ 判準:加一個新的收件者,需要改發送者嗎?
需要 → 收件人清單。不需要 → 發布訂閱。

★ 這兩者都是正當的模式,但常被搞混:
很多團隊以為自己在做事件驅動,實際上程式碼裡有一串 if hasX { publish(…) }
——那是收件人清單,而且它的耦合是真實存在的(見 發布訂閱通道)。

★ 什麼時候收件人清單才是對的答案

用收件人清單用發布訂閱
★ 清單依「訊息內容」而變(VIP 客戶多通知一個風控服務)收件者固定,或由訂閱者自己決定
★ 發送的是命令(必須有人做,而且要知道是誰)發送的是事件
★ 需要知道「送給了誰」(稽核、合規)不需要
★ 要收集每個收件者的回覆 → 升級成 散發-聚集不需要回覆

★ 最典型的正當場景:比價/招標。
「這個詢價要送給哪三家供應商」是由訊息內容決定的動態清單,
發布訂閱表達不了「只送給這三家」。

★ Go 的實作與三個必須處理的失敗

func send(ctx context.Context, recipients []string, msg Message) error {
    var errs []error
    for _, r := range recipients {
        if err := publish(ctx, r, msg); err != nil {
            errs = append(errs, fmt.Errorf("%s: %w", r, err))   // ★ 不要第一個失敗就中斷
        }
    }
    return errors.Join(errs...)     // ★ Go 1.20+:把多個錯誤合起來
}

★★ 這裡的三個設計問題,EIP 書裡談得很少,但每一個都會出事:

① ★★ 部分失敗
   送給 5 個收件者,第 3 個失敗 → 怎麼辦?
   ★ 重送整則 → 前兩個會收到第二份(★ 所以收件者必須冪等)
   ★ 只重送失敗的 → ★ 需要記錄「誰已經成功了」⇒ 這個元件變成有狀態的
   → ★ 務實解:接受「重送全部」,把冪等做在收件者端([[冪等接收者]])

② ★ 清單為空
   computeRecipients 回傳空清單 → ★ 訊息就這樣消失了,而且沒有錯誤
   → ★★ 一定要明確處理:是正常的(沒有符合的收件者)還是 bug?
      曖昧就送 [[無效訊息通道]] 並計數

③ ★ 清單爆量
   規則寫錯 → 一則訊息送給 10,000 個收件者 → ★ 下游被打爆
   → ★ 設上限並在超過時告警(不要靜默送出去)

★ 清單怎麼算出來(三種來源,代價不同)

來源代價
訊息內容本身(msg.Recipients)★ 最簡單;但發送者要知道通道名
查資料庫/設定★ 每則訊息一次查詢 → 要快取,而快取要失效
呼叫外部服務★★ 同步依賴:那個服務掛了,路由就停了(見 內容擴充器 的同一問題)

★ 兄弟辨析

差別
發布訂閱通道★ 發送者不知道收件者;★ 加一個下游不用改發送者
內容路由器恰好一個目的地
分割器★ 依內容項目拆成不同的訊息;收件人清單是複製同一則
散發-聚集★ 收件人清單 + 聚合器(送出去之後還要收集回覆)

🧪 我實際套用的紀錄

  • (待填)

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

  • 需決定清單如何產生與維護。

  • 送給多個收件者時,要考慮個別失敗的處理。

  • 與 Publish-Subscribe 不同:清單是明確計算出、可控的一組收件者。

  • ★★ 判準:加一個新收件者要改發送者嗎? 要 → 收件人清單;不要 → 發布訂閱。很多「事件驅動」其實是這個模式。

  • ★★ 部分失敗沒有好答案。重送全部(收件者必須冪等)或記錄誰成功了(元件變有狀態)。務實選前者。

  • ★★ 清單為空時訊息會靜默消失。一定要明確判斷是正常還是 bug,曖昧就送無效訊息通道並計數。

  • ★ 清單要有上限。規則寫錯導致送給一萬個收件者會打爆下游。

  • ★ 清單來源若是呼叫外部服務,就引入了同步依賴——那個服務掛了路由就停了。

  • ★ 不要第一個收件者失敗就中斷。用 errors.Join 收集所有錯誤。

  • 收件者必須冪等,因為重送是唯一務實的失敗處理。

  • 需要收集回覆時,這就升級成 散發-聚集,複雜度完全不同(有狀態、要逾時)。

🔗 相關工具