模板引擎:text/template 和 html/template

学习 Go 的模板引擎,掌握 text/template 和 html/template 的使用方法

模板引擎: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 的模板引擎:

  1. 基础语法:字段访问、方法调用、管道
  2. 控制结构:if/else、range、with
  3. 自定义函数:扩展模板功能
  4. 模板组合:define、template
  5. HTML 模板:自动转义和安全的 HTML 类型

模板是 Web 开发和文本生成的重要工具。Go 的模板引擎简洁而强大,能满足大多数场景的需求。

练习时间

  1. 创建一个静态网站生成器,用模板生成 HTML 页面
  2. 实现一个邮件模板系统,支持多个邮件模板
  3. 写一个代码生成工具,用模板生成 Go 代码
  4. 创建一个配置文件生成器(YAML/JSON/TOML)

我们下篇见!

继续阅读

探索更多技术文章

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

全部文章 返回首页