🎯 什麼情境該想到我

當你「想在 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, &lt;script&gt;…                       // ✅ 安全

★ 規則:產生 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,不是領域物件。

🔗 相關工具