模板引擎:text/template 和 html/template
在构建 Web 应用、生成配置文件、发送电子邮件等场景中,我们经常需要把数据填充到预定义的模板中。Go 的标准库提供了两个模板引擎:
text/template:用于生成纯文本(邮件、配置文件、代码等)html/template:用于生成 HTML,自动处理转义防止 XSS 攻击
今天我们就来全面学习 Go 的模板引擎。
基础用法
package main
import (
"os"
"text/template"
)
func main() {
// 定义模板
tmpl := `Hello, {{.Name}}!
You are {{.Age}} years old.
Your email is {{.Email}}.`
// 解析模板
t, err := template.New("greeting").Parse(tmpl)
if err != nil {
panic(err)
}
// 准备数据
data := struct {
Name string
Age int
Email string
}{
Name: "张三",
Age: 25,
Email: "zhangsan@example.com",
}
// 执行模板
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
输出:
Hello, 张三!
You are 25 years old.
Your email is zhangsan@example.com.
模板语法
访问字段
type User struct {
Name string
Age int
Address struct {
City string
ZipCode string
}
}
user := User{
Name: "张三",
Age: 25,
Address: struct {
City string
ZipCode string
}{
City: "北京",
ZipCode: "100000",
},
}
tmpl := `姓名: {{.Name}}
城市: {{.Address.City}}
邮编: {{.Address.ZipCode}}`
t, _ := template.New("user").Parse(tmpl)
t.Execute(os.Stdout, user)
调用方法
type User struct {
FirstName string
LastName string
}
func (u User) FullName() string {
return u.FirstName + " " + u.LastName
}
user := User{FirstName: "三", LastName: "张"}
tmpl := `全名: {{.FullName}}`
t, _ := template.New("user").Parse(tmpl)
t.Execute(os.Stdout, user)
管道(Pipeline)
模板支持管道操作,类似 Unix 的管道:
tmpl := `
大写: {{.Name | upper}}
长度: {{.Name | len}}
组合: {{.Name | upper | printf "Hello, %s!"}}
`
funcMap := template.FuncMap{
"upper": strings.ToUpper,
}
t := template.New("test").Funcs(funcMap)
t, _ = t.Parse(tmpl)
t.Execute(os.Stdout, map[string]string{"Name": "zhangsan"})
控制结构
if/else
tmpl := `
{{if .LoggedIn}}
欢迎回来,{{.Username}}!
{{else}}
请先登录。
{{end}}
{{if gt .Age 18}}
你已成年
{{else if gt .Age 12}}
你是青少年
{{else}}
你是儿童
{{end}}
`
range 循环
data := struct {
Fruits []string
Users []struct {
Name string
Age int
}
}{
Fruits: []string{"苹果", "香蕉", "橙子"},
Users: []struct {
Name string
Age int
}{
{"张三", 25},
{"李四", 30},
},
}
tmpl := `
水果列表:
{{range .Fruits}}
- {{.}}
{{else}}
没有水果
{{end}}
用户列表:
{{range $index, $user := .Users}}
{{$index}}. {{$user.Name}} ({{$user.Age}}岁)
{{end}}
`
t, _ := template.New("list").Parse(tmpl)
t.Execute(os.Stdout, data)
with 块
tmpl := `
{{with .Address}}
城市: {{.City}}
邮编: {{.ZipCode}}
{{else}}
没有地址信息
{{end}}
`
自定义函数
package main
import (
"fmt"
"os"
"strings"
"text/template"
"time"
)
func main() {
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"title": strings.Title,
"repeat": strings.Repeat,
"replace": strings.ReplaceAll,
"add": func(a, b int) int {
return a + b
},
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
"safeHTML": func(s string) string {
return s // 注意:这只是为了演示,实际使用时要谨慎
},
}
tmpl := `
大写: {{.Name | upper}}
小写: {{.Name | lower}}
标题: {{.Name | title}}
重复: {{.Symbol | repeat 5}}
加法: {{add .Price .Tax}}
日期: {{.Date | formatDate}}
`
t := template.New("test").Funcs(funcMap)
t, err := t.Parse(tmpl)
if err != nil {
panic(err)
}
data := struct {
Name string
Symbol string
Price int
Tax int
Date time.Time
}{
Name: "zhang san",
Symbol: "*",
Price: 100,
Tax: 15,
Date: time.Now(),
}
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
模板组合
package main
import (
"os"
"text/template"
)
// 定义多个模板
var templates = `
{{define "header"}}
========== 报告 ==========
{{end}}
{{define "footer"}}
==========================
生成时间: {{.}}
{{end}}
{{define "report"}}
{{template "header" .}}
用户: {{.Name}}
年龄: {{.Age}}
邮箱: {{.Email}}
{{template "footer" .GeneratedAt}}
{{end}}
`
func main() {
t := template.Must(template.New("report").Parse(templates))
data := struct {
Name string
Age int
Email string
GeneratedAt string
}{
Name: "张三",
Age: 25,
Email: "zhangsan@example.com",
GeneratedAt: "2021-03-17 11:30:00",
}
err := t.ExecuteTemplate(os.Stdout, "report", data)
if err != nil {
panic(err)
}
}
从文件加载模板
// templates/email.txt
// 亲爱的 {{.Name}},
//
// 感谢您注册我们的服务!
// 您的账户信息:
// - 用户名:{{.Username}}
// - 邮箱:{{.Email}}
//
// 请点击以下链接激活您的账户:
// {{.ActivationLink}}
//
// 祝好,
// {{.TeamName}}
package main
import (
"os"
"text/template"
)
func main() {
t, err := template.ParseFiles("templates/email.txt")
if err != nil {
panic(err)
}
data := struct {
Name string
Username string
Email string
ActivationLink string
TeamName string
}{
Name: "张三",
Username: "zhangsan",
Email: "zhangsan@example.com",
ActivationLink: "https://example.com/activate?token=abc123",
TeamName: "Example 团队",
}
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
HTML 模板
html/template 会自动转义 HTML 特殊字符,防止 XSS 攻击:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}</h1>
<p>{{.Content}}</p>
{{if .HTMLContent}}
<div>{{.HTMLContent}}</div>
{{end}}
<ul>
{{range .Items}}
<li>{{.}}</li>
{{end}}
</ul>
</body>
</html>
`
t, err := template.New("page").Parse(tmpl)
if err != nil {
panic(err)
}
data := struct {
Title string
Content string
HTMLContent template.HTML
Items []string
}{
Title: "测试页面",
Content: "<script>alert('xss')</script>", // 会被转义
HTMLContent: template.HTML("<strong>这是安全的 HTML</strong>"), // 不会转义
Items: []string{"苹果", "香蕉", "橙子"},
}
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
安全的 HTML 类型
// 标记为安全的 HTML
template.HTML("<strong>bold</strong>")
// 标记为安全的 URL
template.URL("https://example.com")
// 标记为安全的 JavaScript
template.JS("alert('hello')")
// 标记为安全的 CSS
template.CSS("color: red;")
实战:邮件模板系统
package main
import (
"bytes"
"html/template"
"log"
"os"
"path/filepath"
)
type EmailTemplate struct {
templates map[string]*template.Template
funcMap template.FuncMap
}
func NewEmailTemplate(templateDir string) (*EmailTemplate, error) {
funcMap := template.FuncMap{
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
"safeURL": func(s string) template.URL {
return template.URL(s)
},
}
et := &EmailTemplate{
templates: make(map[string]*template.Template),
funcMap: funcMap,
}
// 加载所有模板文件
files, err := filepath.Glob(filepath.Join(templateDir, "*.html"))
if err != nil {
return nil, err
}
for _, file := range files {
name := filepath.Base(file)
tmpl, err := template.New(name).Funcs(funcMap).ParseFiles(file)
if err != nil {
return nil, err
}
et.templates[name] = tmpl
}
return et, nil
}
func (et *EmailTemplate) Render(name string, data interface{}) (string, error) {
tmpl, ok := et.templates[name]
if !ok {
return "", fmt.Errorf("template %s not found", name)
}
var buf bytes.Buffer
err := tmpl.ExecuteTemplate(&buf, name, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func main() {
// 创建模板目录
os.MkdirAll("templates", 0755)
// 创建示例模板
welcomeTmpl := `
<!DOCTYPE html>
<html>
<body>
<h1>欢迎,{{.Name}}!</h1>
<p>感谢您注册 {{.AppName}}。</p>
<p>请点击以下链接激活您的账户:</p>
<a href="{{.ActivationURL | safeURL}}">激活账户</a>
</body>
</html>
`
os.WriteFile("templates/welcome.html", []byte(welcomeTmpl), 0644)
// 使用模板
et, err := NewEmailTemplate("templates")
if err != nil {
log.Fatal(err)
}
data := map[string]interface{}{
"Name": "张三",
"AppName": "MyApp",
"ActivationURL": "https://example.com/activate?token=abc",
}
html, err := et.Render("welcome.html", data)
if err != nil {
log.Fatal(err)
}
fmt.Println(html)
}
小结
今天我们学习了 Go 的模板引擎:
- 基础语法:字段访问、方法调用、管道
- 控制结构:if/else、range、with
- 自定义函数:扩展模板功能
- 模板组合:define、template
- HTML 模板:自动转义和安全的 HTML 类型
模板是 Web 开发和文本生成的重要工具。Go 的模板引擎简洁而强大,能满足大多数场景的需求。
练习时间
- 创建一个静态网站生成器,用模板生成 HTML 页面
- 实现一个邮件模板系统,支持多个邮件模板
- 写一个代码生成工具,用模板生成 Go 代码
- 创建一个配置文件生成器(YAML/JSON/TOML)
我们下篇见!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。