模版语法
变量
渲染template的时候,有两个常用的传入参数的类型。
- 一个是
struct
,在模板内可以读取该struct域的内容来进行渲染。 - 还有一个是
map[string]interface{}
,在模板内可以使用key来进行渲染。
在模板文件内,.
代表了当前变量,即在非循环体内,.
就代表了传入的那个变量。
假设我们定义了一个结构体:
1 | type Article struct { |
那么我们在模板内可以通过以下来获取并把变量的内容渲染到模板内。
1 | <p>{{.ArticleContent}}<span>{{.ArticleId}}</span></p> |
定义变量
1 | {{$article := "hello"}} |
假设我们想要把传入值的内容赋值给article,则可以这样写:
1 | {{$article := .ArticleContent}} |
这样我们只要使用{{$article}}
则可以获取到这个变量的内容。
函数
golang的模板其实功能很有限,很多复杂的逻辑无法直接使用模板语法来表达,所以只能使用模板函数来绕过。
template包创建新的模板的时候,支持.Funcs方法来将自定义的函数集合导入到该模板中,后续通过该模板渲染的文件均支持直接调用这些函数。
该函数集合的定义为:
1 | type FuncMap map[string]interface{} |
key为方法的名字,value则为函数。这里函数的参数个数没有限制,但是对于返回值有所限制。
有两种选择,
- 一种是只有一个返回值,
- 一种是有两个返回值,但是第二个返回值必须是error类型的。
这两种函数的区别是第二个函数在模板中被调用的时候,假设模板函数的第二个参数的返回不为空,则该渲染步骤将会被打断并报错。
在模板文件内,调用方法也非常的简单:
1 | {{funcname .arg1 .arg2}} |
示例:
定义了一个函数
1 | func add(left int, right int) int |
则在模板文件内,通过调用以下即可
1 | {{add 1 2}} |
示例
1 | package main |
判断
仅仅支持最简单的bool类型和字符串类型的判断
1 | {{if .condition}} |
当.condition为bool类型的时候,则为true表示执行,当.condition为string类型的时候,则非空表示执行。
内置的模板函数
1 | {{if not .condition}} |
循环
支持range循环来遍历map、slice内的内容
1 | {{range $i, $v := .slice}} |
还有一种遍历方式为:
1 | {{range .slice}} |
这种方式无法访问到index或者key的值,需要通过.来访问对应的value
1 | {{range .slice}} |
当然这里使用了.来访问遍历的值,那么我们想要在其中访问外部的变量怎么办?(比如渲染模板传入的变量),在这里,我们需要使用$.
来访问外部的变量
1 | {{range .slice}} |
模板提供range
关键字来遍历数据。假如我们又下面的数据结构:
1 | type Item struct { |
ViewData
对象传给模板,模板如下:
1 | {{range .Items}} |
对于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 | <body> |
1 | type person struct { |
上面的例子传入一个person的数据结构,得到它的FavNums字段中的第三个值。
模板的嵌套
当模板想要引入子模板的时候,我们使用以下语句:
1 | {{template "navbar"}} |
这样子就会尝试载入名称为navbar的子模板,同时我们也得定义一个子模板来实现”navbar”这个子模板。
子模板的定义为:
1 | {{define "navbar"}} |
在定义之间的内容将会覆盖{{template “navbar”}}
当然子模板是分离了,那么子模板能否获得父模板的变量呢?这是当然的,我们只需要使用
1 | {{template "navbar" .}} |
就可以将当前的变量传给子模板了,这个也是相当方便的。
解析和创建模板
创建模板
tpl, err := template.Parse(filename)
得到文件名为名字的模板,并保存在tpl
变量中。tpl可以被执行来显示模板。
解析多个模板
template.ParseFiles(filenames)
可以解析一组模板,使用文件名作为模板的名字。
template.ParseGlob(pattern)
会根据pattern
解析所有匹配的模板并保存。
解析字符串模板
t, err := template.New("foo").Parse({{define "T"}}Hello, {{.}}!{{end}})
可以解析字符串模板,并设置它的名字
示例
1 | package main |
1 |
|
- 在go程序中,handler函数中使用
template.ParseFiles("test.html")
,它会自动创建一个模板(关联到变量t1上),并解析一个或多个文本文件(不仅仅是html文件), - 解析之后就可以使用
Execute(w,"hello world")
去执行解析后的模板对象,执行过程是合并、替换的过程。 - 例如上面的
{{.}}
中的.
会替换成当前对象”hello world”,并和其它纯字符串内容进行合并,最后写入w中,也就是发送到浏览器”hello world”。
关于点”.”和作用域
在写template的时候,会经常用到”.”。比如{{.}}
、{{len .}}
、{{.Name}}
、{{$x.Name}}
等等。
在template中,点”.”代表当前作用域的当前对象。它类似于java/c++的this关键字,类似于perl/python的self。如果了解perl,它更可以简单地理解为默认变量$_
。
例如,前面示例test.html中{{.}}
,这个点是顶级作用域范围内的,它代表Execute(w,"hello worold")
的第二个参数”hello world”。也就是说它代表这个字符串对象。
再例如,有一个Person struct。
1 | type Person struct { |
这里{{.Name}}
和{{.Age}}
中的点”.”代表的是顶级作用域的对象p,所以Execute()方法执行的时候,会将{{.Name}}
替换成p.Name
,同理{{.Age}}
替换成{{p.Age}}
。
但是并非只有一个顶级作用域,range、with、if等内置action都有自己的本地作用域。
例如下面的例子,如果看不懂也没关系,只要从中理解”.”即可。
1 | package main |
输出结果:
1 | hello longshuai! |
这里定义了一个Person结构,它有两个slice结构的字段。在Parse()方法中:
- 顶级作用域的
{{.UserName}}
、{{.Emails}}
、{{.Friends}}
中的点都代表Execute()的第二个参数,也就是Person对象p,它们在执行的时候会分别被替换成p.UserName、p.Emails、p.Friends。 - 因为Emails和Friend字段都是可迭代的,在
{{range .Emails}}...{{end}}
这一段结构内部an email {{.}}
,这个”.”代表的是range迭代时的每个元素对象,也就是p.Emails这个slice中的每个元素。 - 同理,with结构内部
{{range .}}
的”.”代表的是p.Friends,也就是各个,再此range中又有一层迭代,此内层{{.Fname}}
的点代表Friend结构的实例,分别是&f1
和&f2
,所以{{.Fname}}
代表实例对象的Fname字段。
去除空白
template引擎在进行替换的时候,是完全按照文本格式进行替换的。除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留。所以,对于要解析的内容,不要随意缩进、随意换行。
可以在{{`符号的后面加上短横线并保留一个或多个空格"- "来去除它前面的空白(包括换行符、制表符、空格等),即`{{- xxxx`。
在`}}
的前面加上一个或多个空格以及一个短横线”-“来去除它后面的空白,即xxxx -}}
。
例如:
1 | {{23}} < {{45}} -> 23 < 45 |
其中{{23 -}}
中的短横线去除了这个替换结构后面的空格,即}} <
中间的空白。同理{{- 45}}
的短横线去除了< {{`中间的空白。
再看上一节的例子中:
自身也占一行,在替换的时候它会被保留为空行。除非range前面没加1
2
3
4
5
6
7
8
9
10t.Parse(
`hello {{.UserName}}!
{{ range .Emails }}
an email {{ . }}
{{- end }}
{{ with .Friends }}
{{- range . }}
my friend name is {{.Fname}}
{{- end }}
{{ end }}`){{-`。由于range的`{{- end`加上了去除前缀空白,所以每次迭代的时候,每个元素之间都换行输出但却不多一空行,如果这里的end去掉`{{-`,则每个迭代的元素之间输出的时候都会有空行。同理后面的with和range。
# 注释
注释方式:`{{/* a comment */}}
。
注释后的内容不会被引擎进行替换。但需要注意,注释行在替换的时候也会占用行,所以应该去除前缀和后缀空白,否则会多一空行。
1 | {{- /* a comment without prefix/suffix space */}} |
注意,应该只去除前缀或后缀空白,不要同时都去除,否则会破坏原有的格式。例如:
1 | t.Parse( |
管道pipeline
pipeline是指产生数据的操作。比如{{.}}
、{{.Name}}
、funcname args
等。
可以使用管道符号|
链接多个命令,用法和unix下的管道类似:|
前面的命令将运算结果(或返回值)传递给后一个命令的最后一个位置。
例如:
1 | {{.}} | printf "%s\n" "abcd" |
{{.}}
的结果将传递给printf,且传递的参数位置是”abcd”之后。
命令可以有超过1个的返回值,这时第二个返回值必须为err类型。
需要注意的是,并非只有使用了|
才是pipeline。Go template中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。这使得某些操作可以作为另一些操作内部的表达式先运行得到结果,就像是Unix下的命令替换一样。
例如,下面的(len "output")
是pipeline,它整体先运行。
1 | {{println (len "output")}} |
下面是Pipeline的几种示例,它们都输出"output"
:
1 | {{`"output"`}} |
变量
可以在template中定义变量:
1 | // 未定义过的变量 |
例如:
1 | {{- $how_long :=(len "output")}} |
再例如:
1 | tx := template.Must(template.New("hh").Parse( |
输出结果:
1 | 44 333 444 |
上面的示例中,使用range迭代slice,每个元素都被赋值给变量$x
,每次迭代过程中,都新设置一个变量$y
,在内层嵌套的if结构中,可以使用这个两个外层的变量。在if的条件表达式中,使用了一个内置的比较函数gt,如果$x
大于33,则为true。在println的参数中还定义了一个$z
,之所以能定义,是因为($z := 444)
的过程是一个Pipeline,可以先运行。
需要注意三点:
- 变量有作用域,只要出现end,则当前层次的作用域结束。内层可以访问外层变量,但外层不能访问内层变量。
- 有一个特殊变量
$
,它代表模板的最顶级作用域对象(通俗地理解,是以模板为全局作用域的全局变量),在Execute()执行的时候进行赋值,且一直不变。例如上面的示例中,$ = [11 22 33 44 55]
。再例如,define定义了一个模板t1,则t1中的$
作用域只属于这个t1。 - 变量不可在模板之间继承。普通变量可能比较容易理解,但对于特殊变量”.”和”$”,比较容易搞混。见下面的例子。
例如:
1 | func main() { |
上面使用define额外定义了T1和T2两个模板,T2中嵌套了T1。{{template "T2" .}}
的点代表顶级作用域的”hello world”对象。在T2中使用了特殊变量$
,这个$
的范围是T2的,不会继承顶级作用域”hello world”。但因为执行T2的时候,传递的是”.”,所以这里的$
的值仍然是”hello world”。
不仅$
不会在模板之间继承,.
也不会在模板之间继承(其它所有变量都不会继承)。实际上,template可以看作是一个函数,它的执行过程是template("T2",.)
。如果把上面的$
换成”.”,结果是一样的。如果换成{{template "T2"}}
,则$=nil
条件判断
有以下几种if条件判断语句,其中第三和第四是等价的。
1 | {{if pipeline}} T1 {{end}} |
需要注意的是,pipeline为false的情况是各种数据对象的0值:数值0,指针或接口是nil,数组、slice、map或string则是len为0。
range…end迭代
有两种迭代表达式类型:
1 | {{range pipeline}} T1 {{end}} |
range可以迭代slice、数组、map或channel。迭代的时候,会设置”.”为当前正在迭代的元素。
对于第一个表达式,当迭代对象的值为0值时,则range直接跳过,就像if一样。对于第二个表达式,则在迭代到0值时执行else语句。
1 | tx := template.Must(template.New("hh").Parse( |
需注意的是,range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:
1 | {{range $value := .}} |
如果range中只赋值给一个变量,则这个变量是当前正在迭代元素的值。如果赋值给两个变量,则第一个变量是索引值(map/slice是数值,map是key),第二个变量是当前正在迭代元素的值。
下面是在html中使用range的一个示例。test.html文件内容如下:
1 | <html> |
以下是test.html同目录下的go程序文件:
1 | package main |
with…end
with用来设置”.”的值。两种格式:
1 | {{with pipeline}} T1 {{end}} |
对于第一种格式,当pipeline不为0值的时候,点”.”设置为pipeline运算的值,否则跳过。
对于第二种格式,当pipeline为0值时,执行else语句块,否则”.”设置为pipeline运算的值,并执行T1。
例如:
1 | {{with "xx"}}{{println .}}{{end}} |
上面将输出xx
,因为”.”已经设置为”xx”。
内置函数
以下是内置的函数列表:
1 | and |
除此之外,还内置一些用于比较的函数:
1 | eq arg1 arg2: |
对于eq函数,支持多个参数:
1 | eq arg1 arg2 arg3 arg4... |
它们都和第一个参数arg1进行比较。它等价于:
1 | arg1==arg2 || arg1==arg3 || arg1==arg4 |
示例:
1 | {{ if (gt $x 33) }}{{println $x}}{{ end }} |
自定义函数
Go的模板支持自定义函数。
1 | func sayHello(w http.ResponseWriter, r *http.Request) { |
我们可以在模板文件hello.html中使用我们自定义的kua函数了。
1 | {{kua .Name}} |
嵌套template:define和template
define可以直接在待解析内容中定义一个模板,这个模板会加入到common结构组中,并关联到关联名称上。
定义了模板之后,可以使用template这个action来执行模板。template有两种格式:
1 | {{template "name"}} |
第一种是直接执行名为name的template,点设置为nil。第二种是点”.”设置为pipeline的值,并执行名为name的template。可以将template看作是函数:
1 | template("name) |
例如:
1 | func main() { |
输出结果:
1 | ONE <nil> |
上面定义了4个模板,一个是test1,另外三个是使用define来定义的T1、T2、T3,其中t1是test1模板的关联名称。T1、T2、T3和test1共享一个common结构。其中T3中包含了执行T1和T2的语句。最后只要{{template T3}}
就可以执行T3,执行T3又会执行T1和T2。也就是实现了嵌套。此外,执行{{template "T1"}}
时,点设置为nil,而{{temlate "T2" "haha"}}
的点设置为了”haha”。
注意,模板之间的变量是不会继承的。
下面是html文件中嵌套模板的几个示例。
t1.html文件内容如下:
1 |
|
因为内部有{{template "t2.html"}}
,且此处没有使用define去定义名为”t2.html”的模板,所以需要加载解析名为t2.html的文件。t2.html文件内容如下:
1 | <div style="background-color: yellow;"> |
处理这两个文件的handler函数如下:
1 | func process(w http.ResponseWriter, r *http.Request) { |
上面也可以不额外定义t2.html文件,而是直接在t1.html文件中使用define定义一个模板。修改t1.html文件如下:
1 |
|
然后在handler中,只需解析t1.html一个文件即可。
1 | func process(w http.ResponseWriter, r *http.Request) { |
block块
1 | {{block "name" pipeline}} T1 {{end}} |
根据官方文档的解释:block等价于define定义一个名为name的模板,并在”有需要”的地方执行这个模板,执行时将”.”设置为pipeline的值。
但应该注意,block的第一个动作是执行名为name的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板。换句话说,block可以认为是设置一个默认模板。
例如:
1 | {{block "T1" .}} one {{end}} |
它首先表示{{template "T1" .}}
,也就是说先找到T1模板,如果T1存在,则执行找到的T1,如果没找到T1,则临时定义一个{{define "T1"}} one {{end}}
,并执行它。
下面是正常情况下不使用block的示例。
home.html文件内容如下:
1 | <html> |
在此文件中指定了要执行一个名为”content”的模板,但此文件中没有使用define定义该模板,所以需要在其它文件中定义名为content的模板。现在分别在两个文件中定义两个content模板:
red.html文件内容如下:
1 | {{ define "content" }} |
blue.html文件内容如下:
1 | {{ define "content" }} |
在handler中,除了解析home.html,还根据需要解析red.html或blue.html:
1 | func process(w http.ResponseWriter, r *http.Request) { |
如果使用block,那么可以设置默认的content模板。例如将原本定义在blue.html中的content设置为默认模板。
修改home.html:
1 | <html> |
然后修改handler:
1 | func process(w http.ResponseWriter, r *http.Request) { |
当执行else语句块的时候,发现home.html中要执行名为content的模板,但在ParseFiles()中并没有解析包含content模板的文件。于是执行block定义的content模板。而执行非else语句的时候,因为red.html中定义了content,会直接执行red.html中的content。
block通常设置在顶级的根文件中,例如上面的home.html中。
html/template的上下文感知
对于html/template包,有一个很好用的功能:上下文感知。text/template没有该功能。
上下文感知具体指的是根据所处环境css、js、html、url的path、url的query,自动进行不同格式的转义。
例如,一个handler函数的代码如下:
1 | func process(w http.ResponseWriter, r *http.Request) { |
上面content是Execute的第二个参数,它的内容是包含了特殊符号的字符串。
下面是test.html文件的内容:
1 | <html> |
上面test.html中有4个不同的环境,分别是html环境、url的path环境、url的query环境以及js环境。虽然对象都是{{.}}
,但解析执行后的值是不一样的。如果使用curl获取源代码,结果将如下:
1 | <html> |
不转义
上下文感知的自动转义能让程序更加安全,比如防止XSS攻击(例如在表单中输入带有<script>...</script>
的内容并提交,会使得用户提交的这部分script被执行)。
如果确实不想转义,可以进行类型转换。
1 | type CSS |
转换成指定个时候,字符都将是字面意义。
例如:
1 | func process(w http.ResponseWriter, r *http.Request) { |