--- tags: [后端, Go, Gin, 模板, HTML] create time: 2026-04-28 00:05 --- # HTML 模板渲染 ## 概述 Gin 内建了对 Go 标准库 `html/template` 的封装,支持简单模板加载、多模板引擎配置、以及通过 `embed.FS` 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。 思考题:`c.HTML` 和 `http.ServeFile` 直接返回 `.html` 文件有什么区别? ## 正文 ### 1. 基本模板渲染 Gin 通过 `gin.H`(即 `map[string]any`)将数据传给 Go 标准库 `html/template`。Gin 内部调用 `template.ParseFiles()` 加载模板,并以**文件名(不带路径)**作为模板名建立映射: ```go func main() { r := gin.Default() // 加载 templates/ 目录下所有 .html 文件 r.LoadHTMLGlob("templates/*") r.GET("/index", func(c *gin.Context) { // 渲染 templates/index.html,传入模板数据 // Gin 模板的 key 使用大驼峰(类似结构体字段),Go 的 html/template 按反射访问 c.HTML(http.StatusOK, "index", gin.H{ "Title": "Home Page", "User": "wonder", "Items": []string{"item1", "item2"}, }) }) r.GET("/news", func(c *gin.Context) { c.HTML(http.StatusOK, "news.html", gin.H{ "Title": "News", "Items": []string{"item1", "item2"}, }) }) r.Run(":8080") } ``` 模板文件 `templates/index.html`: ```html
{{.Body | truncate 50}}
发布于 {{.Date | formatDate}} ``` > [!WARNING] SetFuncMap 必须先于 LoadHTMLGlob 调用 > Gin 将 `SetFuncMap` 的设置缓存在内部,因此必须在 `LoadHTMLGlob()` / `LoadHTMLFiles()` **之前**设置。否则注册的函数不会生效,模板中出现自定义管道符时会报 "undefined function" 错误。 ### 4. 模板继承(Base Template) Go 原生模板不支持继承,但可以通过 `define` + `block` + `template` 模拟页面布局系统:`base.html` 定义骨架和可替换区块(block),子模板用 `define` 覆盖对应区块。 > [!INFO] block vs define 的区别 > - `{{block "name" .}}...{{end}}` — 用于 **base.html** 中定义默认内容,子模板可以选择性覆盖 > - `{{define "name"}}...{{end}}` — 用于 **子模板** 中实现自己的版本来覆盖 base 中的默认内容 > - 两者配合才能实现"继承"效果 ```html{{.Message}}
{{end}} ``` 渲染时传入组合后的模板: ```go r.LoadHTMLFiles( "templates/base.html", "templates/index.html", ) // LoadHTMLFiles 将多个文件加载到同一个 *template.Template 中, // 因此 index.html 中的 define 可以找到 base.html 中 block 的定义并合并输出。 ``` > [!TIP] 更复杂的场景 → 多模板引擎 > 如果项目需要按模块隔离不同的模板集(如后台管理一套模板、前台展示一套模板),可以使用第三方库 [gin-contrib/multitemplate](https://github.com/gin-contrib/multitemplate),它允许在同一 Router 下注册多个独立的 `*template.Template` 对象。详见本章「进阶用法」部分。 ### 5. 将模板打包进单一二进制(Go 1.16+) 使用 `embed.FS` 把模板文件嵌入 Go 编译产物,适合 Docker 单镜像部署: ```go import _ "embed" import "html/template" //go:embed templates/*.html var templateFS embed.FS func main() { r := gin.Default() // Go 1.23+:ParseFS 直接返回 *template.Template,配合 Must 处理错误 t := template.Must(template.New("").ParseFS(templateFS, "templates/*.html")) r.SetHTMLTemplate(t) r.Run(":8080") } ``` > [!INFO] 原理 > `SetHTMLTemplate` 替换 Gin 内部的默认模板对象(`*template.Template`),之后所有 `c.HTML()` 调用都走这个已嵌入的模板集。如果只需加载单个新模板而非全部替换,也可以用 `t.AddParseTree("name", tree)` 追加。 > [!NOTE] 项目目录结构 > > ``` > cmd/ > ├── server/main.go ← embed 入口 > templates/ ← 模板文件,不会被 gitignore > ├── base.html > ├── index.html > └── error.html > internal/ > └── handlers/ > ``` > > `embed` 指令位于 `main.go` 所在目录下执行,`templates/` 是相对于 `main.go` 的路径。部署时只需一个二进制文件,不再需要挂载 Volume 或复制模板文件到容器中。 ### 6. 模板安全注意事项 > [!INFO] 安全原则 > 永远不要手动拼接入用户可控的 HTML 内容。Go 的 `html/template` 包根据**上下文自动选择转义策略**,使用默认字符串类型即可获得最安全的输出。只有在明确需要渲染富文本时才考虑绕过转义。 ```mermaid 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: ```go // ❌ 危险:用户输入未过滤就标为 safe c.HTML(200, "page", gin.H{ "content": template.HTML(userInput), // 可能被注入