🎯 什麼情境該想到我
當你「想在 HTML 樣板中嵌入標記、填入動態資料來產生頁面」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:以 HTML 為主體,在其中放入特定標記(佔位符),執行時把動態資料填進去產生最終頁面。
做法要點:
- 直接寫好 HTML 版型,在需要動態內容處放置標記(如 JSP、模板引擎語法)。
- 執行期由樣板引擎解析標記、代入資料,輸出完整 HTML。
- 版型接近成品,設計人員較易參與。
- 常見於 JSP、ASP、各式模板引擎。
★★ Go 的 html/template:它的自動轉義是「上下文感知」的
go doc html/template 查證(2026-09-25):
「escaping 是上下文相關的(contextual),所以 action 可以出現在
JavaScript、CSS、URI 的上下文中。」
// ★ 同一個 {{.}} 在不同位置會套用不同的轉義函式(解析時就改寫好)
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
// ↑ URL 上下文用 urlescaper ↑ 文字上下文用 htmlescaper★★ 這是 html/template 相對 text/template 的全部價值:
text/template → Hello, <script>alert('pwned')</script>! // ★★ XSS
html/template → Hello, <script>… // ✅ 安全★ 規則:產生 HTML 一律用 html/template,絕不用 text/template。
★★ 但它的安全模型有一個關鍵前提,很多人不知道
官方文件原文:
「本套件使用的安全模型假設樣板作者是被信任的,而
Execute的 data 參數不是。」
// ★★ 所以這兩件事完全不安全:
tmpl, _ := template.New("x").Parse(userSuppliedTemplate) // ★★ 使用者提供樣板 = RCE 級風險
out := template.HTML(userInput) // ★★ 明確繞過轉義
// ★ template.HTML / JS / CSS / URL 這些型別的意思是「我保證這是安全的」
// → ★★ 對它們傳入使用者輸入,等於自己關掉防護★★ template.HTML(userInput) 是 Go Web 最常見的 XSS 來源——
它看起來只是一次型別轉換。
★ 正確做法:要允許使用者提供富文字 → 用 HTML sanitizer(如 bluemonday)先清洗,
再包成 template.HTML。
★★ 五個實務要點
// ① ★★ 樣板要在啟動時解析一次,不要每次請求 Parse
var tmpl = template.Must(template.ParseFS(embedFS, "templates/*.html"))
// ★ 每次請求 Parse:① 慢(解析 + 轉義分析)② ★ 錯誤在請求時才發現
// ★★ template.Must + 啟動時解析 = 樣板語法錯誤讓程式「啟動失敗」而非「線上 500」
// ② ★★ 用 embed 把樣板打進二進位檔
//go:embed templates
var embedFS embed.FS
// ★ 好處:部署只有一個檔案、不會「忘記複製樣板」、不會被改
// ③ ★★ 執行時的錯誤處理:Execute 可能寫了一半才失敗
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "order.html", view); err != nil {
http.Error(w, "render failed", 500) // ★ 還沒寫進 w,所以能回 500
return
}
buf.WriteTo(w) // ★★ 先渲染到緩衝區,成功才寫出
// ★ 直接 Execute(w, …) 失敗時,客戶端已經收到一半的 HTML(★ 且狀態碼是 200)
// ④ ★ 樣板裡不要有邏輯 —— 尤其不要有 I/O
{{ .Order.Customer.Name }} // ★★ 若 Customer 是 lazy getter → N+1,而且看不出來
// → ✅ ★ 傳進去的必須是「已經組好的 view struct」([[兩步驟視圖]] 的論點)
// ⑤ ★ {{ }} 裡的函式要在 FuncMap 註冊,而註冊的函式也要注意轉義
tmpl.Funcs(template.FuncMap{"money": formatMoney}) // ★ 回傳 string 會被轉義(正確)★★ ③ 的「先渲染到緩衝區」是最容易被忽略、後果最尷尬的一條:
直接 Execute(w, …) 在中途失敗時,使用者看到半頁 HTML + 200 狀態碼。
★★ 樣板視圖 vs 前後端分離:判準
| 選樣板視圖(SSR) | 選 轉換視圖(JSON API + 前端渲染) |
|---|---|
| ★ SEO 重要(雖然今天爬蟲能跑 JS,但首屏內容仍有優勢) | 高互動性應用 |
| ★ 首屏速度重要、客戶端裝置弱 | 已有前端團隊與 build pipeline |
| ★ 團隊只有後端工程師 | 多個客戶端(Web + iOS + Android) |
| ★ 頁面以「讀」為主(文件、部落格、電商列表) | 複雜的客戶端狀態 |
| ★★ 想避免「兩套模型 + 一層 DTO」的成本 | — |
★★ 不要二元選擇:htmx 式的「伺服器渲染 HTML 片段 + 少量 JS」
讓你用 html/template 拿到大部分互動性——★ 對後端為主的團隊,這常常是最划算的點。
★ 兄弟辨析
| 差別 | |
|---|---|
| 轉換視圖 | ★ 遍歷資料產生輸出(JSON 編碼/XSLT);樣板視圖是「在樣板裡挖洞」 |
| 兩步驟視圖 | ★ 先組邏輯結構、再渲染樣式(樣板視圖的兩階段版) |
| 資料傳輸物件 | ★ 傳進樣板的 view struct 就是它 |
| 模型-視圖-控制器 | 樣板視圖是它的 V |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
容易誘使開發者把程式邏輯塞進樣板,難維護、難測試。
-
應把邏輯抽到輔助物件(helper),樣板只做呈現。
-
需要對輸出做大量條件轉換時,轉換視圖可能更合適。
-
★★ 產生 HTML 一律用
html/template,絕不用text/template。 後者不轉義。 -
★★
html/template的安全模型假設「樣板作者被信任」(官方文件原文)。使用者提供的樣板 = RCE 級風險。 -
★★
template.HTML(userInput)是 Go Web 最常見的 XSS 來源。要允許富文字就先用 sanitizer 清洗。 -
★★ 先渲染到
bytes.Buffer,成功才寫出。 直接Execute(w, …)中途失敗 → 使用者看到半頁 HTML + 200 狀態碼。 -
★★ 啟動時
template.Must解析一次,讓語法錯誤變成「啟動失敗」而非「線上 500」。 -
★★ 樣板裡不要有 I/O。
{{ .Order.Customer.Name }}若 Customer 是 lazy getter 就是 N+1,而且在樣板裡完全看不出來。 -
★ 用
//go:embed把樣板打進二進位檔,避免「忘記複製樣板」。 -
★ 不要在 SSR 與 JSON API 之間二元選擇:htmx 式的 HTML 片段常常是後端團隊最划算的點。
-
傳進樣板的必須是已組好的 view struct,不是領域物件。
🔗 相關工具
- 轉換視圖 Transform View(相對方案:用轉換程式產生輸出)
- 兩步驟視圖 Two Step View(可在其第二步中以樣板實作)
- 模型-視圖-控制器 Model View Controller(樣板視圖是其視圖角色的一種實作)
- 企業應用架構模式
- 轉換視圖 —— 另一種 View 實作
- 兩步驟視圖 —— 樣板視圖的兩階段版
- 資料傳輸物件 —— 傳進樣板的 view struct
- 模型-視圖-控制器 —— 樣板視圖是它的 V
- 前端控制器 —— 渲染前的橫切處理