--- tags: [后端, Go, Gin, 模板, HTML] create time: 2026-04-28 00:00 --- # HTML 模板渲染 ## 概述 Gin 内建了对 Go 标准库 `html/template` 的封装,支持简单模板加载、多模板引擎配置、以及通过 `embed.FS` 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。 思考题:`c.HTML` 和 `http.ServeFile` 直接返回 `.html` 文件有什么区别? ## 正文 ### 1. 基本模板渲染 ```go 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`: ```html {{.Title}}

Welcome, {{.User}}

{{range .Items}}
  • {{.}}
  • {{end}} ``` > **关键点:** 第二个参数是模板文件名(不带路径),Gin 根据 `LoadHTMLGlob` 或 `LoadHTMLFiles` 配置的映射来查找。 ### 2. `LoadHTMLGlob` vs `LoadHTMLFiles` | 方法 | 用途 | 示例 | |------|------|------| | `LoadHTMLGlob(pattern)` | 按 glob 模式加载一批模板 | `"templates/**/*.html"` | | `LoadHTMLFiles(paths...)` | 指定具体的文件列表 | `"templates/base.html", "templates/index.html"` | ```go // 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: ```go 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") } ``` 模板中使用: ```html

    {{.Title | upper}}

    {{.Body | truncate 50}}

    发布于 {{.Date | formatDate}} ``` ### 4. 模板继承(Base Template) Go 原生模板不支持继承,但可以通过 `define` + `template` 模拟: ```html {{block "title" .}}Default{{end}} {{block "content" .}}{{end}} {{define "title"}}Home{{end}} {{define "content"}}

    Welcome

    {{.Message}}

    {{end}} ``` 渲染时传入组合后的模板: ```go r.LoadHTMLFiles( "templates/base.html", "templates/index.html", ) // 此时 index.html 中的 {{template "base"}} 才能找到定义 ``` ### 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() // 从 embedded FS 加载模板 r.LoadHTMLTemplates(template.New("").ParseFS(templateFS, "templates/*.html")) r.Run(":8080") } ``` ```go // 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. 模板安全注意事项 ```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), // 可能被注入