This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/10-template-rendering.md
T

5.9 KiB
Raw Blame History

tags, create time
tags create time
后端
Go
Gin
模板
HTML
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 自动转义
})

关联笔记