🎯 什麼情境該想到我

當你「訊息本體要在傳輸途中附帶路由、安全、關聯等額外資訊,又不想污染原始內容,希望進出系統時能一致地包上與拆掉」的時候。

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

意圖:把原始訊息內容包進一個標準「信封」,信封攜帶訊息系統或中介需要的資訊(如路由位址、安全憑證、關聯 ID),內容本體維持原樣。

做法 / 結構要點:

  • 進入訊息系統前,用包裝器把應用資料放進信封的內容區,並填入信封標頭欄位。
  • 訊息在系統中依信封標頭被路由、驗證、追蹤。
  • 抵達目標端點前,由對應的拆封器移除信封、還原原始內容交給應用。

★ 信封的三層與各自的責任

{
  "message_id":     "01J...",              // ★ 冪等去重([[冪等接收者]])
  "correlation_id": "ord-42",              // ★ 串起一次流程([[關聯識別碼]])
  "causation_id":   "01J...",              // ★ 我被哪一則引起
  "type":           "orders.order.placed", // ★ 路由依據,不必解 data
  "schema_version": 2,                     // ★ [[格式指示器]]
  "occurred_at":    "2026-09-25T10:00:00Z",// ★ 業務時間 ≠ 送出時間
  "traceparent":    "00-4bf9...-01",       // ★ W3C 標準,OTel 認得
  "data":           { ... }                // ★ 業務內容,對信封不透明
}

★★ 最重要的一條原則:路由與運維資訊在信封,業務資料在 data。
違反它的代價很具體:路由器必須解開 data 才能決定去向 ⇒ 每個 schema 改一次
就要改路由器
(見 訊息路由器)。

★ Go 的實作:延遲解析是關鍵

type Envelope struct {
    MessageID     string          `json:"message_id"`
    CorrelationID string          `json:"correlation_id"`
    Type          string          `json:"type"`
    SchemaVersion int             `json:"schema_version"`
    OccurredAt    time.Time       `json:"occurred_at"`
    Data          json.RawMessage `json:"data"`     // ★★ 不立刻解
}

★ json.RawMessage 讓路由器、稽核、DLQ 處理器都能處理任何型別的訊息,
而完全不必認識 data 的結構——它們只讀信封。
只有最終的業務處理器才依 Type 與 SchemaVersion 解開 data。

★★ 標頭欄位 vs 信封欄位:一個常被忽略的選擇

放 broker 的標頭放 JSON 信封裡
★ broker 可以直接用來路由/過濾(RabbitMQ headers exchange、SNS filter policy)broker 看不到
★ 不必反序列化整則訊息就能讀要先解 JSON
★ 受 broker 的型別與大小限制無限制
★★ 跨 broker 搬運時容易掉(見 訊息橋接)★ 跟著 body 走,不會掉

★ 務實做法:兩邊都放。
信封裡是真相(跨 broker 不會掉),標頭是「給基礎設施用的副本」(路由、過濾、取樣)。
⚠️ 但要有一個明確的規則:不一致時以信封為準,否則除錯時你不知道該信哪個。

★★ 這件事必須在「第一天」就做

★ 事後加信封極貴:
① 所有生產者都要改(★ 而且過渡期必須同時支援有/無信封兩種格式)
② 所有消費者都要改解析邏輯
③ ★ 已經在通道裡、已經落地的歷史訊息沒有信封
→ ★★ 實務做法:把信封做進共用的 client library,讓開發者「不必想起」它
   (見 [[訊息匯流排]] 的共用 library 討論)

★ 什麼時候不需要信封

  • ★ protobuf/Avro + schema registry:★ 格式指示器已經在 wire format 裡
    (Confluent 的 magic byte + schema ID),而標頭走 broker header
    → 這時「JSON 信封」是多餘的一層。
  • ★ CloudEvents:如果你用它,信封已經被標準化了——
    不要自己再發明一套(它定義了 id/source/type/time/data 等欄位)。
  • 內部單一通道、單一生產者與消費者、一起部署。

★ 兄弟辨析

差別
格式指示器★ 信封裡的一個欄位
內容擴充器/內容過濾器動的是 data 的內容;信封包裝器只動外層
寄物單★ data 太大時,信封裡只放位址
訊息三層結構的總覽

🧪 我實際套用的紀錄

  • (待填)

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

  • 包 / 拆兩端要對信封格式有一致約定,否則無法互通。

  • 多層包裝會增加處理成本與複雜度。

  • 信封只該放傳輸 / 管理用的中介資訊,別把業務邏輯塞進來。

  • ★★ 路由與運維資訊在信封、業務資料在 data。 違反的代價是路由器被每種 schema 綁死。

  • ★ 用 json.RawMessage 延遲解析,讓路由器/稽核/DLQ 處理器不必認識業務型別。

  • ★★ 這件事必須第一天就做。事後加要改所有生產者與消費者,且過渡期要同時支援兩種格式。做進共用 library。

  • ★ 標頭與信封兩邊都放,但要明定「不一致時以信封為準」,否則除錯時不知道信哪個。

  • ★ 跨 broker 搬運時標頭容易掉(見 訊息橋接),信封欄位不會。

  • ★ 用 protobuf + schema registry 或 CloudEvents 時,不要自己再發明一層信封。

  • 信封不是放什麼都可以的地方。它會變成垃圾桶(塞進 tenant、locale、feature flag…)——每個欄位都要有明確的擁有者與用途。

  • 信封欄位一旦有消費者依賴,就是公共契約,不能隨便改名。

🔗 相關工具