官方定义:
Package template implements data-driven templates for generating textual output.
template 包是数据驱动的文本输出模板,其实就是在写好的模板中填充数据。
模板
什么是模板?
下面是一个简单的模板示例:
// 模板定义
tepl := “My name is {{ . }}”
// 解析模板
tmpl, err := template.New(“test”).Parse(tepl)
// 数据驱动模板
data := “jack”
err = tmpl.Execute(os.Stdout, data)
复制代码{{ 和 }} 中间的句号 . 代表传入模板的数据,根据传入的数据不同渲染不同的内容。
. 可以代表 go 语言中的任何类型,如结构体、哈希等。
至于 {{ 和 }} 包裹的内容统称为 action,分为两种类型:
数据求值(data evaluations)
控制结构(control structures)
action 求值的结果会直接复制到模板中,控制结构和我们写 Go 程序差不多,也是条件语句、循环语句、变量、函数调用等等…
将模板成功解析(Parse)后,可以安全地在并发环境中使用,如果输出到同一个 io.Writer 数据可能会重叠(因为不能保证并发执行的先后顺序)。
Actions
模板中的 action 并不多,我们一个一个看。
注释
{{/* comment */}}
复制代码裁剪空格
// 裁剪 content 前后的空格
{{- content -}}
// 裁剪 content 前面的空格
{{- content }}
// 裁剪 content 后面的空格
{{ content -}}
复制代码文本输出
{{ pipeline }}
复制代码pipeline 代表的数据会产生与调用 fmt.Print 函数类似的输出,例如整数类型的 3 会转换成字符串 “3” 输出。
条件语句
{{ if pipeline }} T1 {{ end }}
{{ if pipeline }} T1 {{ else }} T0 {{ end }}
{{ if pipeline }} T1 {{ else if pipeline }} T0 {{ end }}
// 上面的语法其实是下面的简写
{{ if pipeline }} T1 {{ else }}{{ if pipeline }} T0 { {end }}{{ end }}
{{ if pipeline }} T1 {{ else if pipeline }} T2 {{ else }} T0 {{ end }}
复制代码如果 pipeline 的值为空,不会输出 T1,除此之外 T1 都会被输出。
空值有 false、0、任意 nil 指针、接口值、数组、切片、字典和空字符串 ““(长度为 0 的字符串)。
循环语句
{{ range pipeline }} T1 {{ end }}
// 这个 else 比较有意思,如果 pipeline 的长度为 0 则输出 else 中的内容
{{ range pipeline }} T1 {{ else }} T0 {{ end }}
// 获取容器的下标
{{ range $index, $value := pipeline }} T1 {{ end }}
复制代码pipeline 的值必须是数组、切片、字典和通道中的一种,即可迭代类型的值,根据值的长度输出多个 T1。
define
{{ define “name” }} T {{ end }}
复制代码定义命名为 name 的模板。
template
{{ template “name” }}
{{ template “name” pipeline }}
复制代码引用命名为 name 的模板。
block
{{ block “name” pipeline }} T1 {{ end }}
复制代码block 的语义是如果有命名为 name 的模板,就引用过来执行,如果没有命名为 name 的模板,就是执行自己定义的内容。
也就是多做了一步模板是否存在的判断,根据这个结果渲染不同的内容。
with
{{ with pipeline }} T1 {{ end }}
// 如果 pipeline 是空值则输出 T0
{{ with pipeline }} T1 {{ else }} T0 {{ end }}
{{ with arg }}
. // 此时 . 就是 arg
{{ end }}
复制代码with 创建一个新的上下文环境,在此环境中的 . 与外面的 . 无关。
参数
参数的值有多种表现形式,可以求值任何类型,包括函数、指针(指针会自动间接取值到原始的值):
布尔、字符串、字符、浮点数、复数的行为和 Go 类似
关键字 nil 代表 go 语言中的 nil
字符句号 . 代表值的结果
以 $ 字符开头的变量则为变量对应的值
结构体的字段表示为 .Field,结果是 Field 的值,支持链式调用 .Field1.Field2
字典的 key 表示为 .Key 结果是 Key 对应的值
如果是结构体的方法集中的方法 .Method 结果是方法调用后返回的值(The result is the value of invoking the method with dot as the receiver)**
方法要么只有一个任意类型的返回值要么第二个返回值为 error,不能再多了,如果 error 不为 nil,会直接报错,停止模板渲染
方法调用的结果可以继续链式调用 .Field1.Key1.Method1.Field2.Key2.Method2
声明变量方法集也可以调用 $x.Method1.Field
用括号将调用分组 print (.Func1 arg1) (.Func2 arg2) 或 (.StructValuedMethod “arg”).Field
这里最难懂的可能就是函数被调用的方式,如果访问结构体方法集中的函数和字段中的函数,此时的行为有什么不同?
写个 demo 测一下:
type T struct {
Add func(int) int
}
func (t *T) Sub(i int) int {
log.Println(“get argument i:”, i)
return i - 1
}
func arguments() {
ts := &T{
Add: func(i int) int {
return i + 1
},
}
tpl := `
// 只能使用 call 调用
call field func Add: {{ call .ts.Add .y }}
// 直接传入 .y 调用
call method func Sub: {{ .ts.Sub .y }}
`
t, _ := template.New(“test”).Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
“y”: 3,
“ts”: ts,
})
}
output:
call field func Add: 4
call method func Sub: 2
复制代码可以得出结论:如果函数是结构体中的函数字段,该函数不会自动调用,只能使用内置函数 call 调用。
如果函数是结构体方法集中的方法,会自动调用该方法,并且会将返回值赋值给 .,如果函数返回新的结构体、map,可以继续链式调用。
变量
action 中的 pipeline 可以初始化变量存储结果,语法也很简单:
$variable = pipeline
复制代码此时,这个 action 声明了一个变量而没有产生任何输出。
range 循环可以声明两个变量:
range $index, $element := pipeline
复制代码在 if、with 和 range 中,变量的作用域拓展到 {{ end }} 所在的位置。
如果不是控制结构,声明的变量的作用域会扩展到整个模板。
例如在模板开始时声明变量:
{{ $pages := .pagination.Pages }}
{{ $current := .pagination.Current }}
复制代码在渲染开始的时候,$ 变量会被替换成 . 开头的值,例如 $pages 会被替换成 .pagenation.Pages。所以在模板间的相互引用不会传递变量,变量只在某个特定的作用域中产生作用。
函数
模板渲染时会在两个地方查找函数:
自定义的函数 map
全局函数 map,这些函数是模板内置的
自定义函数使用 func (t *Template) Funcs(funcMap FuncMap) *Template 注册。
全局函数列表:
and
返回参数之间 and 布尔操作的结果,其实就是 JavaScript 中的逻辑操作符 &&,返回第一个能转换成 false 的值,在 Go 中就是零值,如果都为 true 返回最后一个值。
tpl := “{{ and .x .y .z }}”
t, _ := template.New(“test”).Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
“x”: 1,
“y”: 0,
“z”: 3,
})
output:
0
复制代码or
逻辑操作符 ||,返回第一个能转换成 true 的值,在 Go 中就是非零值,如果都为 false 返回最后一个值。
tpl := “{{ or .x .y .z }}”
t, _ := template.New(“test”).Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
“x”: 1,
“y”: 0,
“z”: 3,
})
output:
1
复制代码call
返回调用第一个函数参数的结果,函数必须有一个或两个回值(第二个返回值必须是 error,如果值不为 nil 会停止模板渲染)
tpl := “call: {{ call .x .y .z }} \n”
t, _ := template.New(“test”).Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
“x”: func(x, y int) int { return x+y},
“y”: 2,
“z”: 3,
})
output:
5
复制代码html
返回转义后的 HTML 字符串,这个函数不能在 html/template 中使用。
js
返回转义后的 JavaScript 字符串。
index
在第一个参数是 array、slice、map 时使用,返回对应下标的值。
index x 1 2 3 等于 x[1][2][3]。
len
返回复合类型的长度。
not
返回布尔类型参数的相反值。
print
等于 fmt.Sprint。
printf
等于 fmt.Sprintf。
println
等于 fmt.Sprintln。
urlquery
对字符串进行 url Query 转义,不能在 html/template 包中使用。
// URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query.
func URLQueryEscaper(args …interface{}) string {
return url.QueryEscape(evalArgs(args))
}
复制代码从源码可以看到这个函数直接调用 url.QueryEscape 对字符串进行转义,并没有什么神秘的。
比较函数
eq: ==
ge: >=
gt: >
le: <=
lt: <
ne: !=
分析两个源码:
// eq evaluates the comparison a == b || a == c || …
func eq(arg1 reflect.Value, arg2 …reflect.Value) (bool, error) {
v1 := indirectInterface(arg1)
k1, err := basicKind(v1)
if err != nil {
return false, err
}
if len(arg2) == 0 {
return false, errNoComparison
}
for _, arg := range arg2 {
v2 := indirectInterface(arg)
k2, err := basicKind(v2)
if err != nil {
return false, err
}
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type’s sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
case k1 == uintKind && k2 == intKind:
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
default:
return false, errBadComparison
}
} else {
switch k1 {
case boolKind:
truth = v1.Bool() == v2.Bool()
case complexKind:
truth = v1.Complex() == v2.Complex()
case floatKind:
truth = v1.Float() == v2.Float()
case intKind:
truth = v1.Int() == v2.Int()
case stringKind:
truth = v1.String() == v2.String()
case uintKind:
truth = v1.Uint() == v2.Uint()
default:
panic(“invalid kind”)
}
}
if truth {
return true, nil
}
}
return false, nil
}
// ne evaluates the comparison a != b.
func ne(arg1, arg2 reflect.Value) (bool, error) {
// != is the inverse of ==.
equal, err := eq(arg1, arg2)
return !equal, err
}
复制代码eq 先判断接口类型是否相等,然后判断值是否相等,没什么特殊的地方。
ne 更是简单的调用 eq,然后取反。
ge、gt、le、lt 与 eq 类似,先判断类型,然后判断大小。
嵌套模板
下面是一个更复杂的例子:
// 加载模板
template.ParseFiles(“templates/”)
// 加载多个模板到一个命名空间(同一个命名空间的模块可以互相引用)
template.ParseFiles(“header.tmpl”, “content.tmpl”, “footer.tmpl”)
// must 加载失败时 panic
tmpl := template.Must(template.ParseFiles(“layout.html”))
// 执行加载后的模板文件,默认执行第一个
tmpl.Execute(w, “test”)
// 如果 tmpl 中有很多个模板,可以指定要执行的模板名
tmpl.ExecuteTemplate(w, “layout”, “Hello world”)
复制代码ExecuteTemplate 指定的名字就是模板文件中 define “name” 的 name。
总结
Parse 系列函数初始化的 Template 类型实例。
Execute 系列函数则将数据传递给模板渲染最终的字符串。
模板本质上就是 Parse 函数加载多个文件到一个 Tempalte 类型实例中,解析文件中的 define 关键字注册命名模板,命名模板之间可以使用 template 互相引用,Execute 传入对应的数据渲染。
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>“中的尖括号会被编码为<h1>A header!</h1>。
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
// Javascript
var cat = {“Name”:”Sam”, “Age” 12}
安全字符串和 HTML注释
默认情况下 html/template会删除模板中的所有注释,这会导致一些问题,因为有些注释是有用的,比如:
1
2
3
我们可以使用自定义的方法创建一个可以返回注释的函数。在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]>” }}
{ { htmlSafe “<![endif]–>” }}
模板变量
. 字符
模板变量可以是boolean, string, character, integer, floating-point, imaginary 或者 complex constant。传给模板这样的数据就可以通过点号.来访问:
1
{ { . }}
如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。
如果字段还是复杂类型,可以链式访问 { { .Struct.StructTwo.Field }}。
模板中的变量
传给模板的数据可以存在模板中的变量中,在整个模板中都能访问。 比如 { {$number := .}}, 我们使用$number作为变量,保存传入的数据,可以使用{ {$number}}来访问变量。
1
2
{ {$number := .}}
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
如果.Name存在,会输出Hello, Name,否则输出Hello, Anonymous。
模板也提供了{ {else if .Name2 }}处理多个分支。
移除空格
往模板中增加不同的值的时候可能会增加一定数量的空格。我们既可以改变我们的模板以便更好的处理它,忽略/最小化这种效果,或者我们还可以使用减号-:
1
上面的例子告诉模板移除 .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}}
{ {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
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”}}
{ {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}}
{ {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>
Filler paragraph
{ {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”}}
Some other stuff here...
{ {else}}
To enable Feature A please upgrade your plan
{ {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”)}}
Some other stuff here...
{ {else}}
To enable Feature B please upgrade your plan
{ {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” }}
需要传入.User和feature-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库,定义了很多的函数:
String Functions: trim, wrap, randAlpha, plural, etc.
String List Functions: splitList, sortAlpha, etc.
Math Functions: add, max, mul, etc.
Integer Slice Functions: until, untilStep
Date Functions: now, date, etc.
Defaults Functions: default, empty, coalesce, toJson, toPrettyJson, toRawJson, ternary
Encoding Functions: b64enc, b64dec, etc.
Lists and List Functions: list, first, uniq, etc.
Dictionaries and Dict Functions: get, set, dict, hasKey, pluck, deepCopy, etc.
Type Conversion Functions: atoi, int64, toString, etc.
File Path Functions: base, dir, ext, clean, isAbs
Flow Control Functions: fail
Advanced Functions
UUID Functions: uuidv4
OS Functions: env, expandenv
Version Comparison Functions: semver, semverCompare
Reflection: typeOf, kindIs, typeIsLike, etc.
Cryptographic and Security Functions: derivePassword, sha256sum, genPrivateKey, etc.
需要注意三点:
变量有作用域,只要出现end,则当前层次的作用域结束。内层可以访问外层变量,但外层不能访问内层变量。
有一个特殊变量$,它代表模板的最顶级作用域对象(通俗地理解,是以模板为全局作用域的全局变量),在Execute()执行的时候进行赋值,且一直不变。例如上面的示例中,$ = [11 22 33 44 55]。再例如,define定义了一个模板t1,则t1中的$作用域只属于这个t1。
变量不可在模板之间继承。普通变量可能比较容易理解,但对于特殊变量”.”和”$”,比较容易搞混。见下面的例子。
例如:
func main() {
t1 := template.New(“test1”)
tmpl, _ := t1.Parse(
`
{{- define “T1”}}ONE {{println .}}{{end}}
{{- define “T2”}}{{template “T1” $}}{{end}}
{{- template “T2” . -}}
`)
_ = tmpl.Execute(os.Stdout, “hello world”)
}
上面使用define额外定义了T1和T2两个模板,T2中嵌套了T1。{{template “T2” .}}的点代表顶级作用域的”hello world”对象。在T2中使用了特殊变量$,这个$的范围是T2的,不会继承顶级作用域”hello world”。但因为执行T2的时候,传递的是”.”,所以这里的$的值仍然是”hello world”。
不仅$不会在模板之间继承,.也不会在模板之间继承(其它所有变量都不会继承)。实际上,template可以看作是一个函数,它的执行过程是template(“T2”,.)。如果把上面的$换成”.”,结果是一样的。如果换成{{template “T2”}},则$=nil