不是所有 Web 页面都需要前端框架
Go 经常被用来写 JSON API,但它也很适合生成简单服务端页面。后台管理、内部工具、报表页、文档页、小型内容站,有时用 html/template 直接渲染 HTML 会比引入前端构建链更省事。页面请求进来,服务端查询数据,套进模板,返回 HTML,流程非常清楚。
Go 标准库有两个模板包:text/template 和 html/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/template 和 text/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 标准库让这件事足够直接。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。