5.9 KiB
5.9 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-04-28 00:00 |
HTML 模板渲染
概述
Gin 内建了对 Go 标准库 html/template 的封装,支持简单模板加载、多模板引擎配置、以及通过 embed.FS 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。
思考题:c.HTML 和 http.ServeFile 直接返回 .html 文件有什么区别?
正文
1. 基本模板渲染
func main() {
r := gin.Default()
// 加载 templates/ 目录下所有 .html 文件
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
// 渲染 templates/index.html,传入模板数据
c.HTML(http.StatusOK, "index", gin.H{
"title": "Home Page",
"user": "wonder",
})
})
r.GET("/news", func(c *gin.Context) {
c.HTML(http.StatusOK, "news.tmpl", gin.H{
"title": "News",
"items": []string{"item1", "item2"},
})
})
r.Run(":8080")
}
模板文件 templates/index.html:
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>Welcome, {{.User}}</h1>
<!-- 遍历切片 -->
{{range .Items}}
<li>{{.}}</li>
{{end}}
</body>
</html>
关键点: 第二个参数是模板文件名(不带路径),Gin 根据
LoadHTMLGlob或LoadHTMLFiles配置的映射来查找。
2. LoadHTMLGlob vs LoadHTMLFiles
| 方法 | 用途 | 示例 |
|---|---|---|
LoadHTMLGlob(pattern) |
按 glob 模式加载一批模板 | "templates/**/*.html" |
LoadHTMLFiles(paths...) |
指定具体的文件列表 | "templates/base.html", "templates/index.html" |
// Glob — 适合模板较多、结构简单的场景
r.LoadHTMLGlob("templates/**/*") // 包括子目录
// Files — 适合明确知道有哪些模板的场景
r.LoadHTMLFiles(
"templates/base.html",
"templates/index.html",
"templates/error.html",
)
陷阱:
LoadHTMLGlob("templates/**/*")会用每个文件的基名作为模板名。如果templates/sub/page.html也被加载,模板名就是sub/page.html——渲染时需写c.HTML(200, "sub/page.html", data)。
3. 多模板引擎(Template.FuncMap)
可以注册自定义模板函数,类似 Python Jinja2 的 filter:
func main() {
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
"truncate": func(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
},
"upper": strings.ToUpper,
})
r.LoadHTMLGlob("templates/*")
r.GET("/article", func(c *gin.Context) {
c.HTML(200, "article", gin.H{
"title": "Gin Templating Guide",
"body": "这是一篇很长的文章...",
"date": time.Now(),
})
})
r.Run(":8080")
}
模板中使用:
<h1>{{.Title | upper}}</h1>
<p>{{.Body | truncate 50}}</p>
<small>发布于 {{.Date | formatDate}}</small>
4. 模板继承(Base Template)
Go 原生模板不支持继承,但可以通过 define + template 模拟:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}Default{{end}}</title>
</head>
<body>
<nav>...</nav>
{{block "content" .}}{{end}}
<footer>...</footer>
</body>
</html>
<!-- templates/index.html — 引用 base -->
{{define "title"}}Home{{end}}
{{define "content"}}
<h1>Welcome</h1>
<p>{{.Message}}</p>
{{end}}
渲染时传入组合后的模板:
r.LoadHTMLFiles(
"templates/base.html",
"templates/index.html",
)
// 此时 index.html 中的 {{template "base"}} 才能找到定义
5. 将模板打包进单一二进制(Go 1.16+)
使用 embed.FS 把模板文件嵌入 Go 编译产物,适合 Docker 单镜像部署:
import _ "embed"
import "html/template"
//go:embed templates/*.html
var templateFS embed.FS
func main() {
r := gin.Default()
// 从 embedded FS 加载模板
r.LoadHTMLTemplates(template.New("").ParseFS(templateFS, "templates/*.html"))
r.Run(":8080")
}
// Go 1.23+ 写法
r.LoadHTMLTemplates(template.Must(template.New("").ParseFS(templateFS, "templates/*.html")))
项目目录结构:
cmd/
├── server/main.go ← embed 入口
templates/ ← 模板文件,不会被 gitignore
├── base.html
├── index.html
└── error.html
internal/
└── handlers/
优势: 部署时只需一个二进制文件,不再需要挂载 Volume 或复制模板文件到容器中。
6. 模板安全注意事项
flowchart LR
A["用户输入"] --> B["存入 gin.H 数据"]
B --> C["传入 c.HTML 渲染"]
C --> D{是否 HTML 转义?}
D -->|"是 ✅"| E["自动转义,安全"]
D -->|"否 ❌"| F["XSS 漏洞"]
style E fill:#e8f5e9
style F fill:#ffebee
Go 的 html/template 包自动对上下文相关内容进行转义:
- 在 HTML body 中 → 转义
<>&"' - 在 attribute 中 → 转义引号和
<>& - 在 JS 上下文中 → 转义
'和<\/
唯一例外: 使用 template.HTML / template.JS 类型包装的内容不会转义——这意味着你主动告诉模板"这段内容是安全的"。滥用会导致 XSS:
// ❌ 危险:用户输入未过滤就标为 safe
c.HTML(200, "page", gin.H{
"content": template.HTML(userInput), // 可能被注入 <script>
})
// ✅ 安全:让模板自己决定如何转义
c.HTML(200, "page", gin.H{
"content": userInput, // html/template 自动转义
})
关联笔记
- GIN/9-response-rendering — 除了 HTML,Gin 还支持 JSON/XML 等多种渲染
- GIN/11-static-files — 静态资源(CSS/JS/图片)通过 Static 路由提供