Go 模板入门:用 html/template 生成安全的 HTML 页面

本文讲解 Go 标准库 html/template 的解析、渲染、数据传递、循环、条件、模板函数和安全转义,适合刚开始写服务端页面的学习者。

不是所有 Web 页面都需要前端框架

Go 经常被用来写 JSON API,但它也很适合生成简单服务端页面。后台管理、内部工具、报表页、文档页、小型内容站,有时用 html/template 直接渲染 HTML 会比引入前端构建链更省事。页面请求进来,服务端查询数据,套进模板,返回 HTML,流程非常清楚。

Go 标准库有两个模板包:text/templatehtml/template。前者适合生成普通文本,后者适合生成 HTML。写网页时应该使用 html/template,因为它会根据上下文自动转义,能减少 XSS 风险。

这篇文章会用一个文章列表页做例子,讲解模板解析、变量访问、条件、循环、模板函数和 handler 中的渲染方式。

第一个模板

定义数据:

type PageData struct {
	Title string
	Name  string
}

模板字符串:

const page = `
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>{{ .Title }}</title>
</head>
<body>
  <h1>{{ .Title }}</h1>
  <p>你好,{{ .Name }}</p>
</body>
</html>
`

解析并渲染:

tmpl, err := template.New("page").Parse(page)
if err != nil {
	return err
}

data := PageData{Title: "Go 模板入门", Name: "小林"}
if err := tmpl.Execute(os.Stdout, data); err != nil {
	return err
}

模板里的 {{ .Title }} 表示访问当前数据对象的 Title 字段。点号 . 是当前上下文。初学时可以把模板理解成“HTML 里嵌入少量数据表达式”。

在 HTTP handler 中渲染

真实服务里通常从文件解析模板:

templates/
└── articles.html

articles.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>{{ .Title }}</title>
</head>
<body>
  <h1>{{ .Title }}</h1>
  <ul>
    {{ range .Articles }}
      <li>{{ .Title }} - {{ .Author }}</li>
    {{ else }}
      <li>暂无文章</li>
    {{ end }}
  </ul>
</body>
</html>

Go 代码:

type Article struct {
	Title  string
	Author string
}

type ArticlesPage struct {
	Title    string
	Articles []Article
}

func articlesHandler(tmpl *template.Template) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		data := ArticlesPage{
			Title: "文章列表",
			Articles: []Article{
				{Title: "Go 入门", Author: "小林"},
				{Title: "模板基础", Author: "阿周"},
			},
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if err := tmpl.ExecuteTemplate(w, "articles.html", data); err != nil {
			log.Printf("render articles: %v", err)
			http.Error(w, "render error", http.StatusInternalServerError)
		}
	}
}

启动时解析模板:

tmpl, err := template.ParseFiles("templates/articles.html")
if err != nil {
	log.Fatal(err)
}

mux := http.NewServeMux()
mux.HandleFunc("/articles", articlesHandler(tmpl))

模板解析应该尽量放在启动阶段,而不是每个请求都解析。这样模板语法错误能尽早暴露,请求时也更快。

条件和循环

条件:

{{ if .LoggedIn }}
  <p>欢迎回来,{{ .UserName }}</p>
{{ else }}
  <p>请先登录</p>
{{ end }}

循环:

{{ range .Tags }}
  <span>{{ . }}</span>
{{ end }}

range 内部的点号会变成当前元素。如果你需要访问外层数据,可以先保存变量:

{{ $siteTitle := .SiteTitle }}
{{ range .Articles }}
  <h2>{{ .Title }}</h2>
  <small>{{ $siteTitle }}</small>
{{ end }}

模板语言故意不复杂。不要把大量业务逻辑写进模板。模板负责展示,复杂判断应该在 Go 代码里提前整理成适合渲染的数据。

比如不要在模板里到处判断文章状态,不如在数据结构里准备好展示文本:

type ArticleView struct {
	Title      string
	StatusText string
}

这样模板只负责输出。

模板函数

可以注册函数:

func formatDate(t time.Time) string {
	return t.Format("2006-01-02")
}

tmpl, err := template.New("articles.html").Funcs(template.FuncMap{
	"formatDate": formatDate,
}).ParseFiles("templates/articles.html")

模板中使用:

<time>{{ formatDate .PublishedAt }}</time>

函数适合格式化日期、截断文本、简单数字格式化。不要在模板函数里访问数据库或调用外部服务。模板渲染应该是纯粹、快速、可预测的过程。

如果函数返回 (value, error),模板执行时会处理错误:

func mustFormat(t time.Time) (string, error) {
	if t.IsZero() {
		return "", fmt.Errorf("zero time")
	}
	return t.Format("2006-01-02"), nil
}

渲染失败会从 Execute 返回错误。

html/template 的安全转义

html/template 会自动转义 HTML:

data := PageData{
	Name: `<script>alert("xss")</script>`,
}

模板:

<p>{{ .Name }}</p>

输出不会执行脚本,而是把特殊字符转义。这是 html/templatetext/template 的重要区别。写 HTML 页面时不要用 text/template

有时你确实有可信 HTML,比如 Markdown 转换后的内容。可以使用 template.HTML,但要非常谨慎:

type ArticlePage struct {
	Title string
	Body  template.HTML
}

只有当内容已经经过可信清洗,或者来源完全受控时,才这样做。用户输入不能直接转成 template.HTML,否则等于绕过安全保护。

小结

Go 的 html/template 很适合生成简单、可靠的服务端页面。它支持字段访问、条件、循环、模板函数和自动 HTML 转义。入门阶段只要掌握解析模板、准备数据、在 handler 中执行模板,就能做出很多内部工具和后台页面。

模板应该保持简单。复杂业务规则放在 Go 代码里,模板只接收整理好的展示数据;模板解析放在启动阶段,渲染错误要记录;用户输入默认让 html/template 转义,不要轻易使用 template.HTML

服务端渲染不是过时技术。对很多小系统来说,它反而是最稳、最少依赖的方案。Go 标准库让这件事足够直接。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页