🎯 什麼情境該想到我
當你「想用一支轉換程式,把領域資料逐項轉成 HTML 輸出」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:以「轉換」為核心——寫程式逐項讀取領域資料,逐一產生對應的 HTML 元素。
做法要點:
- 為每種資料元素定義如何轉成輸出片段,逐項轉換組成整頁。
- 思維是「輸入 → 轉換 → 輸出」,而非在版型上挖洞填值。
- XSLT 是典型代表(把 XML 轉成 HTML)。
- 轉換邏輯是純程式,較易做單元測試。
★★ 今天最常見的轉換視圖就是 json.Marshal
★ PoEAA 的原始例子是 XSLT:遍歷輸入的結構,逐個元素轉成輸出
★★ 今天的等價物:
① json.Marshal(view) ← ★ 最常見,遍歷 struct 產生 JSON
② protobuf 序列化
③ 前端的 React 元件樹 ← ★ 遍歷資料產生 DOM,這也是轉換視圖
★★ 和 樣板視圖 的本質差別:
樣板視圖是「一份輸出骨架 + 在裡面挖洞」;
轉換視圖是「遍歷輸入資料,每種型別知道自己怎麼輸出」。
★ 所以轉換視圖天然處理「結構不固定」的資料(遞迴、多型、任意深度)。
★★ Go 的 JSON 編碼:六個會出事的細節
// ① ★★ nil slice 與空 slice 序列化不同
var a []Item // → null
b := []Item{} // → []
// ★★ 前端 a.map(...) 會炸。★ 一律在組 view 時初始化為空 slice
// ② ★★ int64 傳到 JavaScript 會失真
type View struct{ ID int64 `json:"id"` } // ★ 9007199254740993 → 前端拿到錯的值
type View struct{ ID string `json:"id"` } // ✅ ID 一律用字串
// ③ ★★ omitempty 分不出「零值」與「沒有」
Discount int `json:"discount,omitempty"` // ★ 0 就不輸出 → 前端不知道是 0 還是沒給
Discount *int `json:"discount"` // ✅ 需要區分時用指標
// ④ ★★ time.Time 的零值會輸出 "0001-01-01T00:00:00Z"
PaidAt time.Time `json:"paid_at"` // ★ 看起來像一個真的時間
PaidAt *time.Time `json:"paid_at"` // ✅ 未付款就是 null
// ⑤ ★ 未匯出欄位不會被序列化(★ 這是好事,但會靜默漏掉)
type View struct{ total Money } // ★★ 輸出 {} —— 沒有任何錯誤
// ⑥ ★★ encoding/json 對 HTML 字元會轉義(&, <, > → & 等)
// ★ 這是為了安全(可安全嵌入 <script>),但會讓輸出看起來很怪
// → ★ 要關掉:enc := json.NewEncoder(w); enc.SetEscapeHTML(false)
// ⚠️ 但關掉之後就不能直接嵌進 HTML 了★★ ①④ 是前端最常抱怨的兩件事,而它們在 Go 端完全不會報錯。
★★ 串流輸出:轉換視圖唯一真正的技術優勢
// ★★ 大量資料不要先組成一個大 slice 再 Marshal
// ❌ 十萬筆訂單 → 記憶體裡一份 slice + 一份 JSON bytes
all, _ := q.ListAllOrders(ctx)
json.NewEncoder(w).Encode(all)
// ✅ ★ 邊查邊寫,記憶體恆定
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("["))
first := true
for rows.Next() {
var o Order
if err := rows.Scan(&o…); err != nil { … }
if !first { w.Write([]byte(",")) }
first = false
if err := json.NewEncoder(w).Encode(toView(o)); err != nil { … }
}
w.Write([]byte("]"))
if err := rows.Err(); err != nil { … } // ★★ 必查(見 [[表格資料閘道]])★★ 但串流輸出有一個無法迴避的問題:你已經寫了 200 和一半的 body,
中途失敗時沒辦法改成 500。
★ 三個對策:
① ★★ 改用 NDJSON(每行一個物件)—— ★ 客戶端可以逐行處理,
而且「最後一行不完整」是一個可偵測的錯誤訊號
② ★ 在輸出結尾放一個明確的「完成標記」
③ ★★ 或乾脆分頁,不要串流(★ 多數情況的正確答案)
★ 什麼時候轉換視圖勝過樣板視圖
- ★★ 輸出結構由資料決定(遞迴的樹、多型的節點、任意深度的巢狀)。
★ 樣板要處理遞迴很痛;轉換視圖天然遞迴。 - ★ 同一份資料要輸出多種格式(JSON/CSV/XML)→ ★ 每種格式一個轉換器。
- ★★ 輸出給程式讀而非人讀 → JSON/protobuf。
★ 什麼時候樣板視圖勝過
- ★ 輸出骨架固定、只有少數幾個洞 → ★ 樣板一眼看得出最終長相。
- ★ 非工程師要改版面 → 樣板檔比 Go 程式碼好改。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
版型不像樣板視圖那樣一眼可見,設計人員較難直接參與。
-
複雜版面用轉換描述可能冗長難讀(尤其 XSLT)。
-
需要接近成品的可視版型時,樣板視圖較直覺。
-
★★ nil slice 序列化成
null、空 slice 成[]。 前端.map()會炸。組 view 時一律初始化為空 slice。 -
★★
int64傳到 JavaScript 會失真(53 bit 安全整數)。ID 一律用字串。 -
★★
time.Time的零值輸出"0001-01-01T00:00:00Z",看起來像真的時間。可為空的時間用*time.Time。 -
★★
omitempty分不出「零值」與「沒有」。 需要區分時用指標。 -
★★ 串流輸出無法在中途改成 500(已寫了 200 和一半 body)。用 NDJSON、放完成標記,或乾脆分頁。
-
★ 未匯出欄位不會被序列化,而且沒有任何錯誤。 輸出
{}是常見的困惑來源。 -
★
encoding/json預設轉義 HTML 字元(&→&)。SetEscapeHTML(false)可關,但關了就不能嵌進 HTML。 -
★ 大量資料不要先組成大 slice 再 Marshal,那是記憶體的兩倍。
-
串流時
rows.Err()必查,否則中斷會產生不完整但看起來成功的輸出。
🔗 相關工具
- 樣板視圖 Template View(相對方案:在 HTML 樣板中嵌入標記)
- 兩步驟視圖 Two Step View(常以轉換手法實作其兩個步驟)
- 模型-視圖-控制器 Model View Controller(轉換視圖是其視圖角色的一種實作)
- 企業應用架構模式
- 樣板視圖 —— 固定版面的對照
- 兩步驟視圖 —— 兩階段渲染
- 資料傳輸物件 —— 被轉換的 view struct
- 訊息轉換器 —— 訊息世界的對應物
- 表格資料閘道 —— 串流時 rows.Err() 必查