[译]Golang template 小抄

目录 [−]

  1. 解析和创建模板
    1. 命名模板
    2. 创建模板
    3. 解析多个模板
    4. 解析字符串模板
  2. 执行模板
    1. 执行简单模板
    2. 执行命名的模板
    3. 模板编码和HTML
    4. 上下文编码
    5. 安全字符串和 HTML注释
  3. 模板变量
    1. . 字符
    2. 模板中的变量
  4. 模板动作
    1. if/else 语句
    2. 移除空格
    3. Range
  5. 模板函数
    1. 获取索引值
    2. and 函数
    3. or 函数
    4. not 函数
    5. 管道
  6. 模板比较函数
    1. 比较
  7. 嵌套模板和布局
    1. 嵌套模板
    2. 模板之间传递变量
    3. 创建布局
  8. 模板调用函数
    1. 函数变量 (调用结构体的方法)
    2. 函数变量 (调用)
    3. 自定义函数
    4. 自定义函数 (全局)
    5. 第三方自定义函数

这是 Curtis Vermeeren 做的 Go模板技术的小抄,这是我非常喜欢的形式,可以以一个简短的总结把相关的技术介绍出来。 大家可以看我以前翻译的Go文件操作大全,也是统一风格的文章。我翻译了这篇文章,补充了遗漏的知识点。

Go标准库提供了几个package可以产生输出结果,而text/template 提供了基于模板输出文本内容的功能。html/template则是产生 安全的HTML格式的输出。这两个包使用相同的接口,但是我下面的例子主要面向HTML应用。

解析和创建模板

命名模板

模板没有限定扩展名,最流行的后缀是.tmpl, vim-go提供了对它的支持,并且godoc的例子中也使用这个后缀。Atom 和 GoSublime 对.gohtml后缀的文件提供了语法高亮的支持。通过对代码库的分析统计发现.tpl后缀也被经常使用。当然后缀并不重要,在项目中保持清晰和一致即可。

创建模板

tpl, err := template.Parse(filename)得到文件名为名字的模板,并保存在tpl变量中。tpl可以被执行来显示模板。

解析多个模板

template.ParseFiles(filenames)可以解析一组模板,使用文件名作为模板的名字。template.ParseGlob(pattern)会根据pattern解析所有匹配的模板并保存。

解析字符串模板

t, err := template.New("foo").Parse(\{ {define "T"}}Hello, { {.}}!{ {end}}`)` 可以解析字符串模板,并设置它的名字。

执行模板

执行简单模板

又两种方式执行模板。简单的模板tpl可以通过tpl.Execute(io.Writer, data)去执行, 模板渲染后的内容写入到io.Writer中。Data是传给模板的动态数据。

执行命名的模板

tpl.ExecuteTemplate(io.Writer, name, data)和上面的简单模板类似,只不过传入了一个模板的名字,指定要渲染的模板(因为tpl可以包含多个模板)。

模板编码和HTML

上下文编码

html/template基于上下文信息进行编码,因此任何需要编码的字符都能被正确的进行编码。

例如"<h1>A header!</h1>"中的尖括号会被编码为&lt;h1&gt;A header!&lt;/h1&gt;

template.HTML可以告诉Go要处理的字符串是安全的,不需要编码。template.HTML("<h1>A Safe header</h1>")会输出<h1>A Safe header</h1>,注意这个方法处理用户的输入的时候比较危险。

html/template还可以根据模板中的属性进行不同的编码。(The go html/template package is aware of attributes within the template and will encode values differently based on the attribute.)

Go 模板也可以应用javascript。struct和map被展开为JSON 对象,引号会被增加到字符串中,,用做函数参数和变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Go
type Cat struct {
Name string
Age int
}
kitten := Cat{"Sam", 12}
// Template
<script>
var cat = { {.kitten}}
</script>
// Javascript
var cat = {"Name":"Sam", "Age" 12}

安全字符串和 HTML注释

默认情况下 html/template会删除模板中的所有注释,这会导致一些问题,因为有些注释是有用的,比如:

1
2
3
<!--[if IE]>
Place content here to target all Internet Explorer users.
<![endif]-->

我们可以使用自定义的方法创建一个可以返回注释的函数。在FuncMap中定义htmlSafe方法:

1
2
3
4
5
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
}).ParseFiles("hello.gohtml")

这个函数会产生一模一样的HTML代码,这个函数可以用在模板中保留前面的注释:

1
2
3
{ {htmlSafe "<!--[if IE 6]>" }}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{ { htmlSafe "<![endif]-->" }}

模板变量

. 字符

模板变量可以是boolean, string, character, integer, floating-point, imaginary 或者 complex constant。传给模板这样的数据就可以通过点号.来访问:

1
{ { . }}

如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。

如果字段还是复杂类型,可以链式访问 { { .Struct.StructTwo.Field }}

模板中的变量

传给模板的数据可以存在模板中的变量中,在整个模板中都能访问。 比如 { {$number := .}}, 我们使用$number作为变量,保存传入的数据,可以使用{ {$number}}来访问变量。

1
2
{ {$number := .}}
<h1> It is day number { {$number}} of the month </h1>
1
2
3
4
5
var tpl *template.Template
tpl = template.Must(template.ParseFiles("templateName"))
err := tpl.ExecuteTemplate(os.Stdout, "templateName", 23)

上面的例子我们把23传给模板,模板的变量$number的值是23,可以在模板中使用。

模板动作

if/else 语句

像其它语言,模板支持if/else语句。我们可以使用if检查数据,如果不满足可以执行else。空值是是false, 0、nil、空字符串或者长度为0的字符串都是false。

1
<h1>Hello, { {if .Name}} { {.Name}} { {else}} Anonymous { {end}}!</h1>

如果.Name存在,会输出Hello, Name,否则输出Hello, Anonymous

模板也提供了{ {else if .Name2 }}处理多个分支。

移除空格

往模板中增加不同的值的时候可能会增加一定数量的空格。我们既可以改变我们的模板以便更好的处理它,忽略/最小化这种效果,或者我们还可以使用减号-

1
<h1>Hello, { {if .Name}} { {.Name}} { {- else}} Anonymous { {- end}}!</h1>

上面的例子告诉模板移除 .Name变量之间的空格。我们在end关键字中也加入减号。这样做的好处是在模板中我们通过空格更方便编程调试,但是生产环境中我们不需要空格。

Range

模板提供range关键字来遍历数据。假如我们又下面的数据结构:

1
2
3
4
5
6
7
8
9
10
type Item struct {
Name string
Price int
}
type ViewData struct {
Name string
Items []Item
}

ViewData对象传给模板,模板如下:

1
2
3
4
5
6
{ {range .Items}}
<div class="item">
<h3 class="name">{ {.Name}}</h3>
<span class="price">${ {.Price}}</span>
</div>
{ {end}}

对于Items中的每个Item, 我们输出它的名称和价格。在range中当前的项目变成了{ {.}},它的属性是{ {.Name}}{ {.Price}}

模板函数

模板包提供了一组预定义的函数,下面介绍一些常用的函数。

获取索引值

如果传给模板的数据是map、slice、数组,那么我们就可以使用它的索引值。我们使用{ {index x number}}来访问x的第number个元素, index是关键字。比如{ {index names 2}}等价于names[2]{ {index names 2 3 4}} 等价于 names[2][3][4]

1
2
3
<body>
<h1> { {index .FavNums 2 }}</h1>
</body>
1
2
3
4
5
6
7
8
9
10
type person struct {
Name string
FavNums []int
}
func main() {
tpl := template.Must(template.ParseGlob("*.gohtml"))
tpl.Execute(os.Stdout, &person{"Curtis", []int{7, 11, 94}})
}

上面的例子传入一个person的数据结构,得到它的FavNums字段中的第三个值。

and 函数

and函数返回bool值,通过返回第一个空值或者最后一个值。and x y逻辑上相当于if x then y else x。考虑下面的代码:

1
2
3
4
5
6
7
type User struct {
Admin bool
}
type ViewData struct {
*User
}

传入一个Admin为true的ViewData对象给模板:

1
2
3
4
5
{ {if and .User .User.Admin}}
You are an admin user!
{ {else}}
Access denied!
{ {end}}

结果会显示You are an admin user!, 如果ViewData不包含一个User值,或者Admin为false,显示结果则会是Access denied!

or 函数

类似 and 函数,但是只要遇到 true就返回。or x y 等价于 if x then x else y。 x 非空的情况下y不会被评估。

not 函数

not函数返回参数的相反值:

1
2
3
{ { if not .Authenticated}}
Access Denied!
{ { end }}

管道

函数调用可以链式调用,前一个函数的输出结果作为下一个函数调用的参数。html/template称之为管道,类似于linux shell命令中的管道一样,它采用|分隔。

注意前一个命令的输出结果是作为下一个命令的最后一个参数,最终命令的输出结果就是这个管道的结果。

模板比较函数

比较

html/template提供了一系列的函数用做数据的比较。数据的类型只能是基本类型和命名的基本类型,比如type Temp float3,格式是{ { function arg1 arg2 }}

  • eq: arg1 == arg2
  • ne: arg1 != arg2
  • lt: arg1 < arg2
  • le: arg1 <= arg2
  • gt: arg1 > arg2
  • ge: arg1 >= arg2

eq函数比较特殊,可以拿多个参数和第一个参数进行比较。{ { eq arg1 arg2 arg3 arg4}}逻辑是arg1==arg2 || arg1==arg3 || arg1==arg4

嵌套模板和布局

嵌套模板

嵌套模板可以用做跨模板的公共部分代码,比如 header或者 footer。使用嵌套模板我们就可以避免一点小小的改动就需要修改每个模板。嵌套模板定义如下:

1
2
3
4
5
{ {define "footer"}}
<footer>
<p>Here is the footer</p>
</footer>
{ {end}}

这里定义了一个名为footer的模板,可以在其他模板中使用:

1
{ {template "footer"}}

模板之间传递变量

模板action可以使用第二个参数传递数据给嵌套的模板:

1
2
3
4
5
6
7
8
9
10
11
12
// Define a nested template called header
{ {define "header"}}
<h1>{ {.}}</h1>
{ {end}}
// Call template and pass a name parameter
{ {range .Items}}
<div class="item">
{ {template "header" .Name}}
<span class="price">${ {.Price}}</span>
</div>
{ {end}}

这里我们使用和上面一样的range遍历items,但是我们会把每个name传给header模板。

创建布局

Glob模式通过通配符匹配一组文件名。template.ParseGlob(pattern string)会匹配所有符合模式的模板。template.ParseFiles(files...)也可以用来解析一组文件。

模板默认情况下会使用配置的参数文件名的base name作为模板名。这意味着views/layouts/hello.gohtml的文件名是hello.gohtml,如果模板中有{ {define “templateName”}}的话,那么templateName会用作这个模板的名字。

模板可以通过t.ExecuteTemplate(w, "templateName", nil)来执行, t是一个类型为Template的对象,w的类型是io.Writer,比如http.ResponseWriter,然后是要执行的模板的名称,以及要传入的数据:

main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Omitted imports & package
var LayoutDir string = "views/layouts"
var bootstrap *template.Template
func main() {
var err error
bootstrap, err = template.ParseGlob(LayoutDir + "/*.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
bootstrap.ExecuteTemplate(w, "bootstrap", nil)
}

所有的.gohtml文件都被解析,然后当访问/的时候,bootstrap会被执行。

views/layouts/bootstrap.gohtml定义如下:

views/layouts/bootstrap.gohtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{ {define "bootstrap"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Go Templates</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h1>Filler header</h1>
<p>Filler paragraph</p>
</div>
<!-- jquery & Bootstrap JS -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
</body>
</html>
{ {end}}

模板调用函数

函数变量 (调用结构体的方法)

我们可以调用模板中对象的方法返回数据,下面定义了User类型,以及一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
ID int
Email string
}
func (u User) HasPermission(feature string) bool {
if feature == "feature-a" {
return true
} else {
return false
}
}

当User类型传给模板后,我们可以在模板中调用它的方法:

1
2
3
4
5
6
7
8
9
10
11
{ {if .User.HasPermission "feature-a"}}
<div class="feature">
<h3>Feature A</h3>
<p>Some other stuff here...</p>
</div>
{ {else}}
<div class="feature disabled">
<h3>Feature A</h3>
<p>To enable Feature A please upgrade your plan</p>
</div>
{ {end}}

模板会调用User的HasPermission方法做检查,并且根据这个返回结果渲染数据。

函数变量 (调用)

如果有时HasPermission方法的设计不得不需要更改,但是当前的函数方法有不满足要求,我们可以使用函数(func(string) bool)作为User类型的字段,这样在创建User的时候可以指派不同的函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Structs
type ViewData struct {
User User
}
type User struct {
ID int
Email string
HasPermission func(string) bool
}
// Example of creating a ViewData
vd := ViewData{
User: User{
ID: 1,
Email: "curtis.vermeeren@gmail.com",
// Create the HasPermission function
HasPermission: func(feature string) bool {
if feature == "feature-b" {
return true
}
return false
},
},
}
// Executing the ViewData with the template
err := testTemplate.Execute(w, vd)

我们需要告诉Go模板我们想调用这个函数,这里使用call关键字。把上面的例子修改如下:

1
2
3
4
5
6
7
8
9
10
11
{ {if (call .User.HasPermission "feature-b")}}
<div class="feature">
<h3>Feature B</h3>
<p>Some other stuff here...</p>
</div>
{ {else}}
<div class="feature disabled">
<h3>Feature B</h3>
<p>To enable Feature B please upgrade your plan</p>
</div>
{ {end}}

自定义函数

另外一种方式是使用template.FuncMap创建自定义的函数,它创建一个全局的函数,可以在整个应用中使用。FuncMap通过map[string]interface{}将函数名映射到函数上。注意映射的函数必须只有一个返回值,或者有两个返回值但是第二个是error类型。

1
2
3
4
5
6
7
8
9
// Creating a template with function hasPermission
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(user User, feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).ParseFiles("hello.gohtml")

这个函数hasPermission检查用户是否有某个权限,它会被保存在FuncMap中。注意自定义的函数必须在调用ParseFiles()之前创建。

这个函数在模板中的使用如下:

1
{ { if hasPermission .User "feature-a" }}

需要传入.Userfeature-a参数。

自定义函数 (全局)

我们前面实现的自定义方法需要依赖.User类型,很多情况下这种方式工作的很好,但是在一个大型的应用中传给模板太多的对象维护起来很困难。我们需要改变自定义的函数,让它无需依赖User对象。

和上面的实现类似,我们创建一个缺省的hasPermission函数,这样可以正常解析模板。

1
2
3
4
5
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
return false
},
}).ParseFiles("hello.gohtml")

这个函数在main()中或者某处创建,并且保证在解析文件之前放入到 hello.gohtml 的function map中。这个缺省的函数总是返回false,但是不管怎样,函数是已定义的,而且不需要User,模板也可以正常解析。

下一个技巧就是重新定义hasPermission函数。这个函数可以使用User对象的数据,但是它是在Handler处理中使用的,而不是传给模板,这里采用的是闭包的方式。所以在模板执行之前你死有机会重新定义函数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
user := User{
ID: 1,
Email: "Curtis.vermeeren@gmail.com",
}
vd := ViewData{}
err := testTemplate.Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

在这个Handler中User被创建,ViewData使用这个User对象。hasPermission采用闭包的方式重新定义了函数。{ {if hasPermission "feature-a"}}的的确确没有传入User参数。

第三方自定义函数

除了官方的预定义的函数外,一些第三方也定义了一些函数,你可以使用这些库,避免重复造轮子。

比如sprig库,定义了很多的函数: