Go Template 语法和使用

text/template 和 html/template 是 Golang 标准库提供的两个数据驱动的模板库,通常被用于文本生成和 HTML 生成。本文将介绍 Golang 模板库的语法和使用。

简单 Template#

Golang 通过 text/templatehtml/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 提供了三个内置函数进行文本输出,分别是 printprintfprintln,等价于 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 提供了三个内置函数进行布尔值计算,分别是 andnotor,此外,还提供了一系列比较函数,分别是 eqneltlegtge。不同普通高级语言的布尔表达式形式,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 关键词新建一个作用域,新的作用域对象 . 将被设置为指定的值。类似 rangewith 也有两种实现形式。

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 }}

其他内置函数#

在上文中我们介绍了 andor 等布尔运算函数,也介绍了 eqne 等比较函数,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/templateNew 函数创建了一个模板实例,这里传入的参数是模板的名字。

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 模板库的性能究竟如何,感兴趣的小伙伴不妨自己研究下。