🎯 什麼情境該想到我

當你「希望接收端一看到訊息從哪個通道來,就知道它是什麼型別、該怎麼處理」的時候。

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

意圖:讓每個通道只傳送同一種資料型別的訊息,使接收端能確定該通道所有訊息的處理方式。

做法 / 結構要點:

  • 一種資料型別配一個專屬通道(例如訂單走訂單通道、庫存變更走庫存通道)。
  • 接收端不必檢查訊息內容來判斷型別,通道本身就代表型別。
  • 發送端負責把不同型別的訊息送到對應的通道。

★★ 為什麼值得守:不然每個消費者都得自己當路由器

// ❌ 一個通道混著放三種訊息 —— 每個消費者都要先判斷型別
for msg := range ch {
    switch msg.Type {
    case "order.placed":   …
    case "order.canceled": …     // ★ 我根本不在乎這個,但還是得收下、解析、判斷、丟棄
    case "user.updated":   …     // ★ 而且上游加一種型別,我的 switch 就會走到 default
    }
}

★ 成本是三重的:白收的網路與反序列化、每個消費者一份重複的判斷邏輯、
以及「上游加一種型別」變成所有消費者都要留意的事。

★ 這個模式的實際落地:通道命名規範

★ 型別即通道名(領域.實體.事件.版本)
orders.order.placed.v1
orders.order.canceled.v1
users.user.updated.v1

★ 通道名就是 schema 的識別——消費者訂閱 orders.order.placed.v1,
就知道自己會拿到什麼,不必看 body 再判斷。

RabbitMQ 上很自然(topic exchange + routing key 通配):

routing key: orders.order.placed
binding:     orders.order.*         ← 想收全部訂單事件
binding:     orders.order.placed    ← 只收下單

Kafka 上要在「一個 topic 一種型別」與「一個 topic 一個實體」之間選:

① 一個 topic 一種事件型別 → ★ 純粹,但同一張訂單的事件分散在多個 topic ⇒ 順序沒了
② ★ 一個 topic 一個實體(orders.order.v1,內含 placed/canceled/shipped)
   → 同一個 orderID 的所有事件在同一個分區 ⇒ ★ 順序保證
   → 代價:消費者要自己 switch(但這次是合理的——它們本來就相關)

★★ 判準:這些事件之間有沒有順序關係?
有(同一張訂單的生命週期)→ 選 ②,順序比純粹重要。
沒有(不同實體的獨立事件)→ 選 ①。

★ 這是 Kafka 特有的取捨,EIP 書裡沒有——因為 JMS 沒有分區的概念。

★ 型別安全怎麼落實到 Go

// ★ 讓「通道」與「訊息型別」在型別系統裡綁在一起
type Topic[T any] struct{ name string }
 
var OrderPlaced = Topic[OrderPlacedV1]{name: "orders.order.placed.v1"}
 
func Publish[T any](ctx context.Context, p Producer, t Topic[T], msg T) error { … }
 
Publish(ctx, p, OrderPlaced, OrderPlacedV1{…})   // ★ 傳錯型別編譯不過
Publish(ctx, p, OrderPlaced, UserUpdatedV1{…})   // ★ 編譯錯誤

⚠️ Go 的泛型不能宣告在方法上(func (p *Producer) Publish[T any] 編譯錯誤),
所以 Publish 必須是自由函式。

★ 什麼時候刻意混放

  • ★ 同一個實體的事件序列(見上的選項②)——為了順序而混放是對的。
  • 信封裡帶 type 欄位的通用事件通道(event log/稽核流)——目的就是「全部都收」。
  • 無效訊息通道/死信通道——★ 它們天生混著各種型別。
  • 量很小、只有一個消費者、不會演進。

🧪 我實際套用的紀錄

  • (待填)

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

  • 型別很多時,通道數量也會跟著膨脹,增加管理成本。

  • 若確實需要用一個通道傳多種型別,需改以訊息標頭標示型別並在接收端分派。

  • ★★ Kafka 上「一個 topic 一種型別」會犧牲同一實體的事件順序。有順序關係就該「一個 topic 一個實體」。

  • ★ 混放的成本是三重的:白收的頻寬、每個消費者一份重複判斷、上游加型別影響所有人。

  • ★ 通道名要帶版本,它就是 schema 的識別。

  • ★ DLQ 與無效訊息通道天生混著各種型別,不適用這個模式。

  • Go 的泛型不能宣告在方法上,型別安全的 publish 必須是自由函式。

  • RabbitMQ 的 routing key 通配很方便,但 binding 拓撲要進版控,否則沒人知道訊息為什麼流到那裡。

  • 不要為了「純粹」把一個實體的事件拆成十個 topic。順序通常比純粹重要。

🔗 相關工具