在 Go 语言中,模板机制(text/template 和 html/template 包)提供了一种强大的、数据驱动的方式来生成文本或 HTML 输出。无论你是生成配置文件、邮件内容,还是构建 Web 应用程序的动态页面,理解 Go 模板都是必不可少的。
如果你正在生成 HTML 输出,强烈建议使用 html/template 包,因为它提供了与 text/template 相同的接口,但增加了自动上下文转义(auto-escaping)功能,以确保输出安全,抵御代码注入攻击(如 XSS)。
本文将详细介绍模板的核心语法、控制结构、数据访问和高级功能。
一、 模板基础与核心概念
1. 动作(Actions)与定界符(Delimiters)
模板文本是 UTF-8 编码的普通文本,所有非动作部分都将按原样复制到输出中。
模板中的“动作”(Actions)——即数据评估或控制结构——默认被 {{ 和 }} 包裹。
- 修改定界符: 你可以使用
Template.Delims(left, right)方法来修改定界符。
示例 1.1:基本结构与数据输出(使用 html/template)
package main
import (
"html/template"
"os"
"log"
)
type Inventory struct {
Material string
Count uint
}
func main() {
// 模板定义
const tpl = "{{.Count}} items are made of {{.Material}}"
// 准备数据
sweaters := Inventory{"wool", 17}
// 1. 创建并解析模板
tmpl, err := template.New("test").Parse(tpl)
if err != nil {
log.Fatal(err)
}
// 2. 执行模板
err = tmpl.Execute(os.Stdout, sweaters)
// 输出: 17 items are made of wool
if err != nil {
log.Fatal(err)
}
}
(注意:虽然此示例使用了 html/template,但其基本语法与 text/template 相同。这个例子展示了 text/template 文档中描述的简单结构。)
2. 空白符修剪(Whitespace Trimming)
为了方便模板源码格式化,Go 模板提供了空白符修剪标记:
- 左侧修剪: 如果左定界符后紧跟一个减号和空格,例如
{{- pipeline}},则将修剪紧接在前的所有尾随空白符。 - 右侧修剪: 如果右定界符前是空格和减号,例如
{{pipeline -}},则将修剪紧接在后的所有引导空白符。
示例 1.2:空白符修剪
// 模板源: "{{23 -}} < {{- 45}}"
// 输出: "23<45"
// (在执行 template.Execute 时,前后的空白都被移除)
3. 注释(Comments)
注释以 {{/* 开始,以 */}} 结束,它们在输出中会被丢弃。注释不能嵌套。
{{/* 这是一个注释,不会出现在最终输出中 */}}
{{- /* 带有空白修剪的注释 */ -}}
二、 数据访问和参数(Arguments)
模板的执行是通过将模板应用于一个数据结构来完成的。在模板执行过程中,游标(cursor)由点 . 表示,称为 “dot”,它代表了数据结构中当前位置的值。
1. 基础数据参数
参数(Argument)是一个简单值,可以表示以下内容:
- 常量: Go 语法中的布尔、字符串、字符、整数、浮点数或复数常量。
nil关键字: 代表 Go 中的未类型化 nil。- 点 (
.): 表示当前数据上下文(dot)的值。 - 变量名: 以美元符号
$开头的字母数字字符串(包括$piOver2或单独的$)。 - 圆括号: 用于分组,如
print (.F1 arg1) (.F2 arg2)。
2. 访问结构体字段、Map 键和方法
通过在 . 或变量名后加点来访问数据结构中的元素。
| 语法 | 描述 | 示例 |
|---|---|---|
.Field | 访问当前数据(dot)的字段。字段必须是结构体的一部分。 | {{.User.Name}} |
.Key | 访问当前数据(dot)的 Map 键。Map 键可以是字母数字标识符,不必以大写字母开头。 | {{.MapData.key1}} |
.Method | 访问当前数据(dot)的无参数方法。该方法必须返回一个值,或两个值(第二个为 error)。 | {{.CalculateTotal}} |
| 链式调用 | 字段、键和方法可以深度链式调用。 | {{.Field1.Key1.Method1}} |
| 变量调用 | 字段、键和方法也可以基于变量进行评估。 | {{$user.Name}}, {{$user.Method}} |
注意: 如果参数是指针,模板引擎会自动解引用到其基础类型。
3. 变量(Variables)
你可以在动作中使用变量来捕获 Pipeline 的结果。
| 语法 | 描述 |
|---|---|
$variable := pipeline | 声明并初始化一个变量。该动作不产生输出。 |
$variable = pipeline | 赋值给一个已声明的变量。 |
变量的作用域延伸到声明它的控制结构 (if, with, range) 的 end 动作。当模板执行开始时,特殊变量 $ 被设置为传递给 Execute 的数据参数(即 dot 的起始值)。
三、 控制结构(Control Flow)
1. 条件语句:if, else, else if
if 动作用于检查 Pipeline 的值是否为空 (empty)。
空值定义: false、0、任何 nil 指针或接口值,以及任何长度为零的数组、切片、Map 或字符串。
| 语法 | 描述 |
|---|---|
{{if pipeline}} T1 {{end}} | 如果 Pipeline 不为空,执行 T1。 |
{{if pipeline}} T1 {{else}} T0 {{end}} | 如果 Pipeline 不为空,执行 T1;否则执行 T0。 |
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}} | 简化 if-else 链。 |
示例 3.1:If 语句
// 假设传入数据 b 是一个 struct
// {{ if (gt .Stars 4.0) }}"{{.Name }}" is a great book.{{ else }}"{{.Name}}" is not a great book.{{ end }}
// 如果 .Stars > 4.0 (使用了内置函数 gt),则执行 T1。
2. 迭代语句:range, else, break, continue
range 动作用于遍历数组、切片、Map、整数或通道。
| 语法 | 描述 |
|---|---|
{{range pipeline}} T1 {{end}} | 遍历 Pipeline 的元素。dot 被设置为连续的元素值。如果长度为零,无输出。 |
{{range $index, $element := pipeline}} T1 {{end}} | 声明两个变量:$index 和 $element,分别设置为数组/切片索引或 Map 键,以及元素值。 |
{{range pipeline}} T1 {{else}} T0 {{end}} | 如果 Pipeline 长度为零,执行 T0。 |
{{break}} | 提前结束最内层的 range 循环。 |
{{continue}} | 停止当前迭代,开始 range 循环的下一次迭代。 |
示例 3.2:Range 语句
{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
// 如果 .Items 列表非空,则遍历 Items 并为每个元素生成一个 <div> 标签。
// 如果 .Items 列表为空,则输出 <div><strong>no rows</strong></div>。
3. 上下文切换:with, else
with 动作允许你在局部范围内将 dot 的值设置为 Pipeline 的结果。
| 语法 | 描述 |
|---|---|
{{with pipeline}} T1 {{end}} | 如果 Pipeline 非空,将 dot 设置为 Pipeline 的值,并执行 T1。 |
{{with pipeline}} T1 {{else}} T0 {{end}} | 如果 Pipeline 非空,执行 T1;否则执行 T0。 |
示例 3.3:With 语句
{{with .Gift -}} Thank you for the lovely {{.}}. {{end}}
// 如果 .Gift 不为空,则在 with 块内,. 指代 .Gift 的值。
// 注意这里使用了 {{- 语法来修剪前后的空白符。
四、 Pipeline 和函数(Functions)
1. Pipeline 概念
Pipeline 是一系列命令(command)的序列,它们之间用管道符 | 分隔。
- 执行方式: 前一个命令的结果会被作为最后一个参数传递给后一个命令。
- 命令类型: 命令可以是一个简单值(Argument),也可以是函数或方法调用。
示例 4.1:Pipeline
{{"output" | printf "%s" | printf "%q"}}
// 1. "output" 被传递给第一个 printf。
// 2. 第一个 printf 的结果作为最后一个参数传递给第二个 printf。
// 最终输出: "\"output\""
2. 预定义函数(Predefined Functions)
模板引擎默认提供了一系列全局预定义函数。
| 函数类别 | 函数名 | 描述 |
|---|---|---|
| 逻辑 | and, or, not | 布尔逻辑操作。非零值视为 true,零值视为 false。 |
| 比较 | eq, ne, lt, le, gt, ge | 等于 (==), 不等于 (!=), 小于, 小于等于, 大于, 大于等于。eq 可以接受两个或多个参数,检查第二个及后续参数是否等于第一个参数。 |
| 数据处理 | len | 返回参数的整数长度。 |
| 格式化 | print, printf, println | 分别是 fmt.Sprint, fmt.Sprintf, fmt.Sprintln 的别名。 |
| 索引/切片 | index | 通过参数索引其第一个参数,如 index x 1 2 相当于 Go 中的 x。 |
slice | 对第一个参数进行切片操作,如 slice x 1 2 相当于 Go 中的 x[1:2]。 | |
| 调用 | call | 调用第一个参数(必须是函数类型),其余参数作为其参数。 |
注意 (安全转义): 在
text/template中,还存在html、js和urlquery等转义函数。但在用于 HTML 输出的html/template包中,这些函数通常是不可用或被禁止的,因为html/template会进行自动上下文转义。
3. 自定义函数
你可以通过 Template.Funcs 方法向模板添加自定义函数,使用 FuncMap。
- 时间要求:
Funcs必须在模板被解析之前调用。 - 返回值: 自定义函数必须返回一个值,或两个值(第二个值必须是
error类型)。
示例 4.2:添加自定义函数
package main
import (
"os"
"strings"
"text/template"
"log"
)
func main() {
// 定义 FuncMap,注册自定义函数 (例如 strings.ToLower, strings.Repeat)
funcMap := template.FuncMap{
"lower": strings.ToLower,
"repeat": func(s string) string { return strings.Repeat(s, 2) },
}
const tmpl = `{{ . | lower | repeat }}`
// 1. 创建模板,通过 Funcs 添加函数,然后解析
parsedTmpl, err := template.New("t").Funcs(funcMap).Parse(tmpl)
if err != nil {
log.Fatal(err)
}
// 2. 执行模板
err = parsedTmpl.Execute(os.Stdout, "ABC")
// 输出: abcabc
}
(此例展示了如何将 lower 和 repeat 函数链接在 Pipeline 中使用。)
五、 模板关联与复用
Go 模板可以关联零个或多个其他模板,形成一个模板命名空间。
1. 模板定义与调用
| 动作 | 描述 |
|---|---|
{{define "name"}} T1 {{end}} | 定义一个名为 "name" 的模板 T1。定义必须出现在模板的顶层。 |
{{template "name"}} | 执行名为 "name" 的关联模板,数据上下文为 nil。 |
{{template "name" pipeline}} | 执行名为 "name" 的关联模板,数据上下文设置为 Pipeline 的值。 |
{{block "name" pipeline}} T1 {{end}} | 块(Block)是定义模板 {{define "name"}} T1 {{end}} 然后立即执行 {{template "name" pipeline}} 的简写。常用于定义根模板,以便通过重定义块模板来自定义内容。 |
示例 5.1:模板关联与调用
假设我们定义了 T1、T2 和 T3 模板:
{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}
// 最终输出: ONE TWO
2. 模板解析与管理方法
Template 类型提供了一系列方法来加载、管理和执行模板。
| 方法/函数 | 描述 |
|---|---|
template.New(name string) | 分配一个新的模板实例。 |
template.Must(t, err) | 包装一个返回 (*Template, error) 的函数调用,如果出错则 panic。 |
t.Parse(text string) | 解析文本作为模板主体。 |
template.ParseFiles(filenames ...string) | 创建新模板并从指定文件解析定义。返回的模板名称是第一个文件的基础名。 |
template.ParseGlob(pattern string) | 创建新模板并解析匹配通配符模式的文件中的定义。 |
t.ParseFiles(...) / t.ParseGlob(...) | 将文件中的模板定义解析并关联到现有模板 t。 |
t.Clone() | 返回模板及其所有关联模板的副本,创建新的命名空间。 |
t.Execute(wr, data) | 对解析后的模板应用指定数据,并写入 wr。 |
t.ExecuteTemplate(wr, name, data) | 执行与 t 关联的、具有指定名称的模板。 |
t.Lookup(name string) | 查找与 t 关联的、具有给定名称的模板。 |
六、 HTML 模板的安全特性 (html/template)
当你使用 html/template 包时,它会自动应用安全措施。
1. 上下文自动转义(Contextual Autoescaping)
html/template 理解 HTML、CSS、JavaScript 和 URI 等上下文。它会在解析时,根据数据插入的位置,自动添加必要的转义函数,以防止恶意内容破坏 HTML 结构。
html/template 自动转义示例:
// 假设数据 {{.}} 是一个包含攻击脚本的字符串:
// "<script>alert('you have been pwned')</script>"
// 使用 html/template 执行模板 `Hello, {{.}}!`:
// 生产安全的、转义后的 HTML 输出:
// Hello, <script>alert('you have been pwned')</script>!
在解析时,模板引擎可能会将简单的动作 {{.}} 改写为包含内部转义函数的 Pipeline,例如:
<a href="/search?q={{.}}">{{.}}</a>
<!-- 在解析时可能变为 (内部别名): -->
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
2. 区分命名空间和 data- 属性
html/template 在确定上下文时,会特殊处理带有命名空间的属性(如 <a my:href="{{.}}">,会被视为 href 属性)。同时,带有 data- 前缀的属性也会被特殊处理。
3. 类型化字符串(Typed Strings)
默认情况下,模板引擎假设所有 Pipeline 产生的是纯文本字符串,并对其进行转义。
如果你确定某些数据是安全且受信任的内容(例如,已经通过 HTML 净化器处理过),你可以将其标记为特定的类型,从而豁免自动转义。
使用类型化字符串存在安全风险: 封装的内容必须来自可信赖的来源,因为它将被原样包含在模板输出中。
| 类型 | 描述 | 危险提示 |
|---|---|---|
template.HTML | 封装已知安全的 HTML 文档片段。 | tmpl.Execute(out, template.HTML(World)) |
template.CSS | 封装已知安全的 CSS 内容(如样式表、规则或声明)。 | 安全风险高。 |
template.JS | 封装已知安全的 EcmaScript5 表达式。 | 安全风险高。 |
template.URL | 封装已知安全的 URL 或 URL 子字符串。 | 用于绕过 javascript: 等协议过滤。 |
template.JSStr | 封装用于嵌入 JavaScript 表达式引号中的字符序列。 | 安全风险高。 |
template.HTMLAttr | 封装 HTML 属性(例如 dir="ltr")。 | 安全风险高。 |
template.Srcset | 封装安全的 srcset 属性。 | 安全风险高。 |
4. 运行时错误和过滤
如果模板执行时,非信任数据在 CSS 或 URL 上下文中评估出不安全的内容(例如,URL 为 javascript:...),html/template 不会输出该内容,而是使用特殊值 #ZgotmplZ 替换它。
// 模板: <img src="{{.X}}">
// 假设 .X 评估为 `javascript:...`
// 输出: <img src="#ZgotmplZ">
总结
Golang 模板引擎提供了一个简洁而强大的语法,通过 . 和 | 实现数据访问和处理,并通过 if/range/with 等动作实现流程控制。在使用 html/template 时,开发者可以受益于自动转义的安全机制,但必须了解上下文转义的工作原理和使用类型化字符串(如 template.HTML)带来的安全责任。理解这些基础和安全特性,你就能编写出高效且安全的 Go 应用程序模板。
类比理解: Go 模板就像一个智能化的厨师(模板引擎)在厨房(Go 程序)中工作。你提供给厨师食谱(模板文件)和食材(数据结构)。厨师会自动将食材安全地整合到食谱中。如果食材是纯文本,厨师会进行必要的防护(如 HTML 转义);如果你明确告诉厨师这块食材(如 template.HTML)是已经消毒且安全的,厨师才会直接使用,但风险自负。








