text/template 和 html/template 是 Golang 标准库提供的两个数据驱动的模板库,通常被用于文本生成和 HTML 生成。本文将介绍 Golang 模板库的语法和使用。
简单 Template
Golang 通过 text/template
和 html/template
提供模板能力,两个库的功能基本一致,区别在于后者可以避免 HTML 注入。两个库均提供了 Template
类型,通过输入模板文本或模板文件构建 Template
实例后,即可通过 Execute
方法生成最终文本或 HTML。
下面是一个简单的示例,我们在模板中使用 {{ . }}
作为变量占位符,然后在生成文本的时候再传入变量。
1
2
3
4
5
6
7
8
9
10
11
| // 构建模板
tmpl, err := template.New("").Parse("你好,{{ . }}")
if err != nil {
panic(err)
}
// 输出文本, 最终输出 "你好,冰镇"
err = tmpl.Execute(os.Stdout, "冰镇")
if err != nil {
panic(err)
}
|
Template 语法
文本和空白
在模板文本中,一切动态的内容和判断代码块均使用 {{
和 }}
包括起来,比如上述的示例 {{ . }}
,在 {{
和 }}
之外的文本均会被原封不动地拷贝到输出中。
为了方便格式化模板源代码,还额外提供了 {{-
和 -}}
两种语法,可以将代码块前或代码块后的空白字符均移除。空白字符包括空格符、换行符、回车符、水平制表符。
1
2
3
4
| {{ 12 }} < {{ 34 }} {{/* => 12 < 34 */}}
{{ 12 }} < {{- 34 }} {{/* => 12 <34 */}}
{{ 12 -}} < {{ 34 }} {{/* => 12< 34 */}}
{{ 12 -}} < {{- 34 }} {{/* => 12<34 */}}
|
注释
模板文本中可以使用 {{/* xxx */}}
记录相关注释,注释不会被最终生成到文本中。但是需要注意的,注释前后的空格和换行均会保留,如果需要去除也可以使用 {{-
和 -}}
。
1
2
| {{/* 这是一个注释 */}}
{{- /*这也是一个注释*/ -}}
|
作用域
在前面的例子,我们使用 {{ . }}
输入了一个变量,这里的 .
表示当前作用域的对象值。在该例子中,当前作用域即为全局作用域,因此 .
实际上就是我们执行 Execute 时传入的变量。
整个模板文件、单个 range 模块、单个 with 模块、单个 block 模块等都可以是一个作用域。
作用域对象也可以传入一个更复杂的结构体。
1
2
3
4
5
6
7
8
9
10
| type Params struct {
UserName string
SiteName string
}
tmpl, _ := template.New("").Parse("你好,这里是{{ .UserName }}的{{ .SiteName }}。")
_ = tmpl.Execute(os.Stdout, Params{
UserName: "MegaShow",
SiteName: "冰镇",
})
|
通过 $
可以访问全局作用域的对象值,以上例子中 {{ .UserName }}
等价于 {{ $.UserName }}
。
字符串格式化
Template 提供了三个内置函数进行文本输出,分别是 print
、printf
、println
,等价于 fmt 包中的 Sprint、Sprintf、Sprintln。
1
2
3
| {{ print 12 }} {{/* => 12 */}}
{{ printf "%03d" 12 }} {{/* => 012 */}}
{{ println 12 }} {{/* => 12\n */}}
|
管道
在 Template 中,一切能产生数据的表达式都是管道 (Pipeline),比如 {{ . }}
是一个管道,{{ print 12 }}
也是一个管道。
类似 Linux 管道操作一样,Template 的管道与管道之间可以通过 |
操作符进行数据传递,可以将前者的数据传递给后者,作为后者的参数进行使用。
1
2
| {{ 12 | printf "%03d" }} {{/* 等价于 {{ printf "%03d" 12 }} */}}
{{ 3 | printf "%d+%d=%d" 1 2 }} {{/* 等价于 {{ printf "%d+%d=%d" 1 2 3 }} */}}
|
变量
在 Template 中可以提前定义变量,等到需要的时候再使用变量。变量均以 $
+ 标识符的形式命名,每个变量仅在它所声明的作用域、以及该作用域所包含的作用域下有效。
1
2
3
| {{ $userName := "MegaShow" -}} {{/* 这里要使用 -}} 去掉额外的换行 */}}
{{ $realName := $userName -}}
{{ $realName | printf "Hello, %s." }} {{/* => Hello, MegaShow. */}}
|
布尔运算
Template 提供了三个内置函数进行布尔值计算,分别是 and
、not
、or
,此外,还提供了一系列比较函数,分别是 eq
、ne
、lt
、le
、gt
、ge
。不同普通高级语言的布尔表达式形式,Template 的布尔表达式必须以前缀表达式的形式实现,即布尔值计算符号后面跟着所需要的参数。
not
对后面的一个参数进行布尔取反。如果参数并不是布尔值时,则根据参数是否非空值进行判断。
1
2
3
4
5
6
| {{/* false => true */}}
{{/* true => false */}}
{{/* nil => true */}}
{{/* "" => true */}}
{{/* "MegaShow" => false */}}
{{ not . }}
|
and
对后面两个参数进行布尔与运算。如果参数并不是布尔值时,则类似 JavaScript 的语法,返回第一个为空的参数或者最后一个参数。
1
2
3
4
5
| {{/* X:false Y:true => false */}}
{{/* X:true Y:true => true */}}
{{/* X:"" Y:true => "" */}}
{{/* X:"MegaShow" Y:"icytown" => "icytown" */}}
{{ and .X .Y }} // 等价于 if .X then .Y else .X
|
or
对后面两个参数进行布尔或运算。如果参数并不是布尔值时,则类似 JavaScript 的语法,返回第一个不为空的参数或最后一个参数。
1
2
3
4
5
| {{/* X:false Y:false => false */}}
{{/* X:false Y:true => true */}}
{{/* X:false Y:"" => "" */}}
{{/* X:"MegaShow" Y:"icytown" => "MegaShow" */}}
{{ or .X .Y }} // 等价于 if .X then .X else .Y
|
我们可以使用以上三种内置函数实现复杂的布尔运算,也可以实现数据获取逻辑。
1
2
3
4
| {{/* X, Y 都不为空 或 Z 不为空 */}}
{{ or (and (not .X) (not .Y)) (not .Z) }}
{{/* 获取 X, Y, Z 中第一个不为空的值,如果都为空则返回 "MegaShow" */}}
{{ or (or (or .X .Y) .Z) "MegaShow" }}
|
比较函数均需要输入两个参数,进行比较并返回 true 或 false。
1
2
3
4
5
6
| {{ eq .X .Y }} {{/* .X == .Y */}}
{{ ne .X .Y }} {{/* .X != .Y */}}
{{ lt .X .Y }} {{/* .X < .Y */}}
{{ le .X .Y }} {{/* .X <= .Y */}}
{{ gt .X .Y }} {{/* .X > .Y */}}
{{ ge .X .Y }} {{/* .X >= .Y */}}
|
条件判断
Template 提供 if else 语句实现条件判断,输入的可以是布尔表达式,也可以非布尔值的变量。
1
2
3
| {{ if pipeline }} A {{ end }}
{{ if pipeline }} A {{ else }} B {{ end }}
{{ if pipeline }} A {{ else if pipeline }} B {{ else }} C {{ end }}
|
循环遍历
Template 可以对一个 slice、array 或 map 进行遍历,支持以下两种形式的语法。
1
2
| {{ range pipeline }} A {{ end }}
{{ range pipeline }} A {{ else }} B {{ end }}
|
当 pipeline 的值为非空值时,将会遍历 pipeline 对象执行 range 里面的逻辑。当 pipeline 的值为空值时,将会执行 else 里面的逻辑。此时,range 和 end 之间将构成一个新的作用域,而作用域的 .
对象的值也会发生变化。当 pipeline 是一个 slice 或 array 时,作用域对象将变成每一个元素的值;当 pipeline 是一个 map 时,作用域对象将变成每一个键值对的值。
1
2
3
4
5
6
7
| {{/* in1 => map[string]string{"1": "abc", "2": "def"} */}}
{{/* out1 => abc\ndef */}}
{{/* in2 => []string{"abc", "def"} */}}
{{/* out2 => abc\ndef */}}
{{ range . -}}
{{ . }}
{{ end -}}
|
为了获取元素的下标值或者键,在遍历的过程中我们可以赋值变量,变量名可以自由定义。这里 $val
实际上等价于新作用域的 .
。
1
2
3
4
5
6
7
8
9
10
11
| {{/* in => map[string]string{"1": "abc", "2": "def"} */}}
{{/* out => 1:abc\n2:def */}}
{{ range $key, $val := . -}}
{{ $key }}:{{ $val }}
{{ end -}}
{{/* in => []string{"abc", "def"} */}}
{{/* out => 0:abc\n1:def */}}
{{ range $idx, $val := . -}}
{{ $idx }}:{{ $val }}
{{ end -}}
|
也可以只赋值一个变量,这样直接获取的是元素值。
1
2
3
| {{ range $val := . -}}
{{ $val }}
{{ end -}}
|
此外,Template 的循环遍历也支持循环跳出的能力,提供了 {{ break }}
和 {{ continue }}
关键词。以下是一个在循环里面使用循环跳出的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| {{/* type => [{Name, HasPermission}] */}}
{{/* in => {{}, {"游客1", false}, {"MegaShow", true}, {"游客2", false}} */}}
{{/* out => 游客1不具备权限。\nMegaShow具备权限。 */}}
{{- range $idx, $user := . -}}
{{- if or (not $user) (not $user.Name) -}}
{{- continue -}}
{{- end -}}
{{- if $user.HasPermission -}}
{{ $user.Name }}具备权限。
{{- break -}}
{{- else -}}
{{ $user.Name }}不具备权限。{{ "\n" }}
{{- end -}}
{{- end -}}
|
with 块
Template 提供了 with
关键词新建一个作用域,新的作用域对象 .
将被设置为指定的值。类似 range
,with
也有两种实现形式。
1
2
| {{ with pipeline }} A {{ end }}
{{ with pipeline }} A {{ else }} B {{ end }}
|
如果 pipeline 不为空,则执行 with 块内的逻辑,并将 .
设置为 pipeline 值;如果 pipeline 为空,则执行 else 块内的逻辑。因此, with
也相当于做一个非空判断。
嵌套模板
Template 支持嵌套,可以通过 define
定义命名模板且通过 template
引用指定模板。这样就可以把一个比较大模板拆分成多个小模板,也可以将重复的逻辑统一封装起来再多次引用。
1
2
3
4
5
6
7
8
9
| {{/* in: {Header: "这是页头", Footer: "这是页脚"} */}}
{{/* out: 这是页头这是页脚 */}}
{{- define "header" -}}{{ . }}{{- end -}}
{{- define "footer" -}}{{ . }}{{- end -}}
{{- define "page" -}}
{{ template "header" .Header -}}
{{ template "footer" .Footer -}}
{{- end -}}
{{- template "page" . -}}
|
通过 template
引用模板时,除了指定模板名外,还需要传递一个参数,这个参数将被传值给引用模板作用域下的 {{ . }}
。
模板也可以引用自身,实现模板递归,不过需要注意应该要使用条件判断或 with
块,防止模板递归死循环。
block
块是特殊的模板,相当于是 define
+ template
的语法糖。
1
2
3
4
5
6
7
| {{/* in: {Header: "这是页头"} */}}
{{/* out: 这是页头 */}}
{{ block "header" .Header }}{{ . }}{{ end }}
{{/* 等价于 */}}
{{ define "header" }}{{ . }}{{ end }}
{{ template "header" .Header }}
|
其他内置函数
在上文中我们介绍了 and
、or
等布尔运算函数,也介绍了 eq
、ne
等比较函数,print
等字符串格式化函数。除了这几类函数外,Template 还提供了以下函数丰富模板能力。
call
函数允许调用非内置函数,第一个参数是函数指针,剩余参数作为调用函数所需的参数,比如 call .Func .X .Y
。
html
函数允许将 HTML 文本转义成安全文本,由于 html/template
本身就会转义 HTML 文本,因此在 html/template
中不存在该内置函数。
index
函数允许获取 slice、array、map、字符串的元素,第一个参数为操作对象,剩余参数为每一维所需的下标或键,比如 index .Arr 0 1 2
等价于 .Arr[0][1][2]
。
slice
函数允许对 Go 切片、数组或字符串进行操作,第一个参数为操作对象,剩下参数为起始下标、截至下标、容量,语法与 Go 相同。比如 slice .X 1
等价于 .X[1:]
,slice .X 1 2
等价于 .X[1:2]
,slice .X 1 2 3
等价于 .X[1:2:3]
。
js
函数允许将 JS 文本转义成安全文本。
len
函数允许获取 slice、array、map、字符串的长度,比如 len .X
等价于 len(.X)
。
urlquery
函数允许将 URL Query 文本转义成安全文本,在 html/template
中不存在该内置函数。
Template 使用
创建简单 Template
在最开始的示例中,我们使用 text/template
的 New
函数创建了一个模板实例,这里传入的参数是模板的名字。
1
| tmpl, _ := template.New("header").Parse("以下为页头的内容: {{ . }}")
|
以上语句其实相当于使用 define
定义了一个名为 header 的模板。
1
| {{ define header }}以下为页头的内容: {{ . }}{{ end }}
|
通过模板实例的 Execute
方法可根据模板生成文本。
1
| tmpl.Execute(os.Stdout, "Hello")
|
还有另一个方法 ExecuteTemplate
也可以根据模板生成文本,不过该方法需要指定模板名称。
1
| tmpl.ExecuteTemplate(os.Stdout, "header", "Hello")
|
创建多个 Template
既然 template.New
本质上也是相当于用 define
定义模板,那么是否有办法通过代码定义多个模板,然后将他们关联起来呢?答案是肯定的,通过模板实例的 New
方法可以创建一个新的模板实例,新的模板与旧模板可以直接关联起来。
1
2
3
4
5
| tmpl, _ := template.New("page").Parse(`{{ template "header" .Header }}{{ template "footer" .Footer }}`)
tmplHeader, _ := tmpl.New("header").Parse("页头内容:{{ . }}")
tmplFooter, _ := tmpl.New("footer").Parse("页脚内容:{{ . }}")
tmpl.Execute(os.Stdout, Params{Header: "header", Footer: "footer"}) // => 页头内容:header页脚内容:footer
|
也可以通过 ExecuteTemplate
使用其他模板输出内容,也可以使用其他模板实例来生成 page 的文本。
1
2
3
4
| tmpl.ExecuteTemplate(os.Stdout, "header", "header") // => 页头内容:header
tmpl.ExecuteTemplate(os.Stdout, "footer", "footer") // => 页脚内容:footer
tmplHeader.ExecuteTemplate(os.Stdout, "page", Params{Header: "header", Footer: "footer"}) // => 页头内容:header页脚内容:footer
tmplFooter.ExecuteTemplate(os.Stdout, "header", "header") // => 页头内容:header
|
HTML Template
html/template
被应用于 HTML 模板,相比 text/template
增加了一系列安全特性。
HTML 模板示例
接下来我们实现一个 Web 服务器,能响应用户请求返回 HTML 页面。
1
2
3
4
5
6
7
8
9
10
11
| <!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta title="首页" />
</head>
<body>
<p>你好,{{ .UserName }}</p>
</body>
</html>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // main.go
var htmlTemplate = template.Must(template.ParseFiles("index.html"))
func handler(w http.ResponseWriter, r *http.Request) {
// 如果不是 GET 则直接返回 404
if r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
htmlTemplate.Execute(w, map[string]any{"UserName": "MegaShow"})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", mux)
}
|
在 handler 中,我们可以根据请求参数获取相应的数据,然后传给 HTML 模板并生成最终 HTML 返回给用户。
HTML 转义
在上节的例子中,实际上使用 text/template
也能达成一样的效果,不过 html/template
额外进行了一系列转义工作,使得模板生成的 HTML 更加安全,以防止外部 XSS 等攻击。
如果我们在这个示例中,我们没有限制用户名的命名规定,或者有其他没有约束的文本要嵌入模板中,如果存在一个用户的昵称为 <script>...</script>
。
1
| htmlTemplate.Execute(w, map[string]any{"UserName": "<script>alert('哈哈')</script>"})
|
在使用 html/template
的时候前端会展示 你好,<script>alert('哈哈')</script>
;而在使用 text/template
时,script 内的 JavaScript 脚本会被执行,浏览器会弹出 哈哈
的警告。如果你没有关注这些安全问题,那么将可能被用户注入并执行更危险的脚本代码。
为了在 html/template
中嵌入原生 HTML 文本,Template 提供了 HTML 函数支持该功能。
1
| htmlTemplate.Execute(w, map[string]any{"UserName": template.HTML("<b>MegaShow</b>")})
|
结语
Golang Template 所提供的功能还是比较实用的,在业界也有一些项目在使用 Golang 特性,比如 Hugo 的 HTML 模板。如果你有类似的模板需求,不妨考虑试试 Go 标准库所提供的模板能力。在我过去的一些工作中,也曾尝试过使用 Go 模板库实现一些模板配置,不过比较可惜的是由于使用场景对性能要求不高,并没有深入了解过 Go 模板库的性能究竟如何,感兴趣的小伙伴不妨自己研究下。