Go html/template 使用入门
用
html/template把 HTML 文件 + Go 数据 → 渲染成完整 HTML,并自动防 XSS。
一、模板是干嘛的
直接拼字符串:
go
html := "<p>你好 " + name + ",验证码是 " + code + "</p>"
两个大问题:
- 不安全 :
name = "<script>alert(1)</script>"直接注入(XSS)。 - 不好维护:HTML 长了之后改样式都得改 Go 代码。
模板 = 把 HTML 放在 .html 文件里,留几个空({``{.Name}}),运行时填数据。
模板文件 + 数据 → 渲染 → 完整 HTML 字符串
二、html/template vs text/template
| 包 | 用途 | 自动转义 |
|---|---|---|
text/template |
任意纯文本(配置、SQL) | ❌ 不转义 |
html/template |
网页、HTML 邮件 | ✅ 按上下文自动转义,防 XSS |
只要输出会被浏览器渲染,一律用 html/template。它能识别上下文:
<p>{``{.X}}</p>按 HTML 规则转义<a href="{``{.X}}">按 URL 规则转义<script>var x = "{``{.X}}";</script>按 JS 字符串规则转义
三、5 分钟跑通
3.1 模板文件
html
<!-- templates/hello.html -->
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<p>你好 {{.Name}},</p>
<p>你的验证码是 <b>{{.Code}}</b>,{{.ExpireMinutes}} 分钟内有效。</p>
</body>
</html>
{``{.Name}} 这种双花括号叫动作 (action)。
3.2 渲染代码
go
package main
import (
"html/template"
"os"
)
func main() {
// 1. 解析模板
tpl, err := template.ParseFiles("templates/hello.html")
if err != nil {
panic(err)
}
// 2. 准备数据(map 或 struct 都行)
data := map[string]any{
"Name": "Alice",
"Code": "123456",
"ExpireMinutes": 5,
}
// 3. 渲染(也可以渲染到 http.ResponseWriter / bytes.Buffer)
if err := tpl.Execute(os.Stdout, data); err != nil {
panic(err)
}
// 渲染bytes.Buffer案例
go
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
panic(err)
}
}
四、解析方式
go
// 单文件
tpl, _ := template.ParseFiles("a.html")
// 多文件
tpl, _ := template.ParseFiles("a.html", "b.html")
// glob 通配
tpl, _ := template.ParseGlob("templates/*.html")
// 字符串
tpl, _ := template.New("t").Parse(`<p>Hi {{.Name}}</p>`)
// 解析失败直接 panic(启动期 fail fast)
tpl := template.Must(template.ParseGlob("templates/*.html"))
多模板渲染指定名字
go
var buf bytes.Buffer
tpl.ExecuteTemplate(&buf, "hello.html", data) // 按 base name
五、模板语法速查
5.1 取数据
html
<p>你好 {{.Name}}</p>
<p>邮箱 {{.Email}}</p>
. 代表整个 data。.Name 在 struct 里就是字段,在 map 里就是键。
⚠️ struct 字段必须首字母大写(导出),否则模板看不见。
5.2 条件判断
html
{{if .IsVIP}}
<span>尊敬的 VIP 用户</span>
{{else if .IsNew}}
<span>新用户</span>
{{else}}
<span>普通用户</span>
{{end}}
判断"非空"用 if .X,零值(空串、0、nil、false)会走 else。
5.3 循环
html
<ul>
{{range .Items}}
<li>{{.Title}} - ¥{{.Price}}</li>
{{else}}
<li>暂无数据</li>
{{end}}
</ul>
range 内部的 . 自动变成当前迭代项。想拿外层数据用 $:
html
{{range .Items}}
<li>{{$.UserName}} 买了 {{.Title}}</li>
{{end}}
要 index:
html
{{range $i, $item := .Items}}
<li>{{$i}}. {{$item.Title}}</li>
{{end}}
5.4 变量
html
{{$name := .User.Name}}
<p>{{$name}}</p>
5.5 with(缩小作用域)
html
{{with .User}}
<p>{{.Name}} - {{.Email}}</p>
{{end}}
with 块里 . 变成 .User。
5.6 管道与函数
html
<p>{{.Name | printf "Hello, %s"}}</p>
| 把左边传给右边的函数。
内置函数:len print printf html js urlquery eq ne lt gt and or not。
html
{{if eq .Status 1}}已支付{{end}}
{{if and .IsVIP (gt .Age 18)}}...{{end}}
5.7 自定义函数
go
funcs := template.FuncMap{
"upper": strings.ToUpper,
"fmtDate": func(t time.Time) string { return t.Format("2006-01-02") },
}
tpl, _ := template.New("hello").Funcs(funcs).ParseFiles("templates/hello.html")
⚠️
Funcs必须在Parse之前调,否则模板里的函数找不到。
模板里用:
html
<p>{{.Name | upper}}</p>
<p>{{fmtDate .CreatedAt}}</p>
六、自动转义(最值钱的特性)
go
data := map[string]any{
"Comment": `<script>alert(1)</script>`,
}
html
<p>{{.Comment}}</p>
渲染结果:
html
<p><script>alert(1)</script></p>
浏览器看到的是字符串,不会执行。
6.1 故意输出原始 HTML
用 template.HTML 类型告诉模板"我知道我在干啥,别转义":
go
data := map[string]any{
"Body": template.HTML(`<b>已经处理过的安全 HTML</b>`),
}
⚠️ 慎用!只有 100% 确认不来自用户输入时才用,否则就是 XSS 漏洞。
类似的:template.JS、template.URL、template.CSS。
七、抽取公共片段:define + template
html
<!-- _header.html -->
{{define "header"}}
<table width="100%"><tr><td>
<h1>{{.AppName}}</h1>
</td></tr></table>
{{end}}
html
<!-- page.html -->
{{template "header" .}}
<p>欢迎 {{.Name}}</p>
{``{template "header" .}} 调用名为 header 的子模板,. 把当前数据传过去。
7.1 母版页(block)
html
<!-- layout.html -->
<html><body>
{{block "content" .}}默认内容{{end}}
</body></html>
html
<!-- page.html -->
{{define "content"}}
<p>这是 page 的内容</p>
{{end}}
go
tpl, _ := template.ParseFiles("layout.html", "page.html")
tpl.ExecuteTemplate(w, "layout.html", data)
八、新手常踩的坑
| 现象 | 原因 | 解决 |
|---|---|---|
template ... not defined |
名字不是文件路径而是 base name | 用 hello.html,或 {``{define "xxx"}} 命名 |
| 中文乱码 | 没 <meta charset="UTF-8"> |
模板首行加上 |
| 字段读不到 | struct 字段小写(不导出)/ map 键拼错 | 字段大写;map 键名要精确 |
输出多了一堆 < > |
普通 string 默认转义 | 确认安全后用 template.HTML 包一下 |
Funcs 不生效 |
在 Parse 之后才注册 |
必须 New().Funcs().Parse() 顺序 |
| 修改模板不生效 | 模板被缓存 | 开发期重新解析或用热重载 |
九、一句话总结
HTML 渲染永远用
html/template:ParseFiles/ParseGlob解析,Execute渲染,{``{.X}}取数据,{``{if}}{``{range}}{``{define}}控制结构,自动按上下文转义防 XSS。