go模板
介绍
template包用于生成文本输出的模板,要生成HTML输出,需要用到html/template
,和text/template
具有相同的接口,但会自动将输出转化为安全的HTML格式输出,可以抵抗一些网络攻击。模板标签用"{{
“和”}}
"括起来。
通过将数据结构作为模板的参数来生成文本。模板中通过引用数据结构的元素(通常是结构的字段或映射中的键)来控制执行过程和需要显示的值。模板执行时会遍历结构并将指针表示为’.’(称之为"dot")指向运行过程中数据结构的当前位置的值。
模板的输入文本格式必须是UTF-8编码。 “Action” - 数据运算或控制结构 - 由“{{
”和“}}
”分隔,在Action之外的所有文本都不做修改的拷贝到输出中。Action内部不能有换行,但注释可以有换行。
一旦解析,模板可以安全地并行执行,但是如果并行执行共享Writer,则输出可以是交错的。
这是一个简单的例子,打印出“17件由羊毛制成”。
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil {
panic(err)
}
空格
默认情况下,执行模板时,将逐字复制actions之间的所有文本。 例如,运行程序时,上例中的字符串" items are made of "出现在标准输出上。但是,为了帮助格式化模板源代码,如果操作的左分隔符(默认为“{{
”)后面紧跟一个减号和ASCII空格字符(“{{-
”),则所有尾随空格都会紧接在文本之前。 同样,如果右边分隔符(“}}
”)前面有空格和减号(“-}}
”),则去除文本中紧随其后的空格。在这些修剪标记中,必须存在ASCII空格字符; “{{-3}}
”解析为数字-3
。
例如,模板
"{{23 }} < {{ 45}}"
"{{23 -}} < {{- 45}}"
输出
"23 < 45"
"23<45"
注释
{{/* a comment */}}
执行时会忽略。可以多行,注释不能嵌套,并且必须紧贴分界符始止,就像这里表示的一样。
package main
import (
"html/template"
"os"
)
func main() {
//创建一个模板
t := template.New("test")
//注释
t, _ = t.Parse(`{{/*我是注释*/}}`)
t.Execute(os.Stdout, nil)
}
输出
{{pipeline}}
pipeline的值的默认文本表示会被拷贝到输出里。
package main
import (
"html/template"
"os"
)
func main() {
//创建一个模板
t := template.New("test")
//显示的值
d := "this is a"
t, _ = t.Parse(`{{.}}{{" test!"}}`)
t.Execute(os.Stdout, d)
//output: this is a test!
}
变量
点号用于输出当前对象的值,$
用于输出模板中定义变量的值。
Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:
$variable := pipeline
其中$variable是变量的名字。声明变量的action不会产生任何输出。
赋值先前声明的变量,语法如下:
$variable = pipeline
如果"range" action初始化了1个变量,该变量设置为迭代器的每一个成员元素,如果初始化了逗号分隔的2个变量:
range $index, $element := pipeline
这时, element分别设置为数组/切片的索引或者字典的键,以及对应的成员元素。注意这和go range从句只有一个参数时设置为索引/键不同!
一个变量的作用域只到声明它的控制结构(“if”、“with”、“range”)的"end"为止,如果不是在控制结构里声明会直到模板结束为止。子模板的调用不会从调用它的位置(作用域)继承变量。
模板开始执行时,$会设置为传递给Execute方法的参数,就是说,dot的初始值。
package main
import (
"os"
"text/template"
)
type User struct {
Id string
UserName string
Age int
Ct Contact
}
type Contact struct {
Name string
Tel int
}
func main() {
//模板函数
t := template.New("test1")
t, _ = t.Parse(`
{{.Id}} {{.UserName}} {{.Age}} {{.Ct.Tel}}
{{$a := .Ct}} {{$a.tel}}
`)
t.Execute(os.Stdout, User{"1234", "wyy", 22,Contact{"mama",195566985}})
//output:
//1234 wyy 22 195566985
}
变量的引用
{{.}}
此标签输出当前对象的值
{{.Admpub}}
表示输出Struct对象中字段或方法名称为“Admpub”的值。
当“Admpub”是匿名字段时,可以访问其内部字段或方法,比如“Com”:{{.Admpub.Com}} ,
如果“Com”是一个方法并返回一个Struct对象,同样也可以访问其字段或方法:{{.Admpub.Com.Field1}}
{{.Method1 "parm1" "parm2"}}
调用方法“Method1”,将后面的参数值依次传递给此方法,并输出其返回值。
{{$admpub}}
此标签用于输出在模板中定义的名称为“admpub”的变量。当KaTeX parse error: Expected '}', got 'EOF' at end of input: …ct对象时,可访问其字段:{{admpub.Field1}}
Arguments表示一个简单的值,由下面的某一条表示的值。
- go语法的布尔值、字符串、字符、整数、浮点数、虚数、复数,视为无类型字面常数,字符串不能跨行
- 关键字nil,代表一个go的无类型的nil值
- 字符’.’(句点,用时不加单引号),代表dot的值
- 变量名,以美元符号起始加上(可为空的)字母和数字构成的字符串,如:$piOver2和$;
执行结果为变量的值,变量参见下面的介绍 - 结构体数据的字段名,以句点起始,如:.Field;
执行结果为字段的值,支持链式调用:.Field1.Field2;
字段也可以在变量上使用(包括链式调用):$x.Field1.Field2; - 字典类型数据的键名;以句点起始,如:.Key;
执行结果是该键在字典中对应的成员元素的值;
键也可以和字段配合做链式调用,深度不限:.Field1.Key1.Field2.Key2;
虽然键也必须是字母和数字构成的标识字符串,但不需要以大写字母起始;
键也可以用于变量(包括链式调用):$x.key1.key2; - 数据的无参数方法名,以句点为起始,如:.Method;
执行结果为dot调用该方法的返回值,dot.Method();
该方法必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
方法可和字段、键配合做链式调用,深度不限:.Field1.Key1.Method1.Field2.Key2.Method2;
方法也可以在变量上使用(包括链式调用):$x.Method1.Field; - 无参数的函数名,如:fun;
执行结果是调用该函数的返回值fun();对返回值的要求和方法一样;函数和函数名细节参见后面。 - 上面某一条的实例加上括弧(用于分组)
执行结果可以访问其字段或者键对应的值:
print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod “arg”).Field
Arguments可以是任何类型:如果是指针,在必要时会自动表示为指针指向的值;如果执行结果生成了一个函数类型的值,如结构体的函数类型字段(返回一个函数指针),该函数不会自动调用,但可以在if等action里视为真。如果要调用它,使用call函数,参见下面。
package main
import (
"os"
"text/template"
)
type User struct {
Id string
UserName string
Age int
}
func main() {
//模板函数
t := template.New("test1")
t, _ = t.Parse(`
{{.Id}} {{.UserName}} {{.Age}}
`)
t.Execute(os.Stdout, User{"1234", "wyy", 22})
//output:
//1234 wyy 22
}
管道
Pipeline是一个(可能是链状的)command序列。Command可以是一个简单值(argument)或者对函数或者方法的(可以有多个参数的)调用:
Argument
执行结果是argument的执行结果
.Method [Argument...]
方法可以独立调用或者位于链式调用的末端,不同于链式调用中间的方法,可以使用参数;
执行结果是使用给出的参数调用该方法的返回值:dot.Method(Argument1, etc.);
functionName [Argument...]
执行结果是使用给定的参数调用函数名指定的函数的返回值:function(Argument1, etc.);
pipeline通常是将一个command序列分割开,再使用管道符’|
'连接起来(但不使用管道符的command序列也可以视为一个管道)。在一个链式的pipeline里,每个command的结果都作为下一个command的最后一个参数。pipeline最后一个command的输出作为整个管道执行的结果。
command的输出可以是1到2个值,如果是2个后一个必须是error接口类型。如果error类型返回值非nil,模板执行会中止并将该错误返回给执行模板的调用者。
{{FuncName1}}
此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1()”,不传递任何参数)并输出其返回值。
{{FuncName1 "parm1" "parm2"}}
此标签将调用“FuncName1(“参数值1”, “参数值2”)”,并输出其返回值
{{.Admpub|FuncName1}}
此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1(this.Admpub)”,将竖线“|
”左边的“.Admpub”变量值作为函数参数传送)并输出其返回值。
package main
import (
"os"
"text/template"
)
type User struct {
Id string
UserName string
Age int
}
//记得函数名首字母大写
func (u User)Test(msg string) string {
return msg
}
//这个函数是注册到模板,可以不用大写
func test1(msg string) string {
return msg + " ==>"
}
func main() {
//模板函数
t1 := template.New("test1")
//注册模板函数
t1.Funcs(template.FuncMap{"test1": test1})
// {{函数名}}输出函数返回值
// {{函数名 参数1 参数2}}
// {{.字段名|函数名}} 以字段的值作为函数的参数
t1, _ = t1.Parse(`
{{.UserName|test1}}
{{ "2018-01-02 15:04:05" | .Test}}
`)
t1.Execute(os.Stdout, User{"1234", "wyy", 22})
//output:
//wyy ==>
//2018-01-02 15:04:05
}
条件
如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典、字符串。
{{if pipeline}} T1 {{end}}
如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。
{{if pipeline}} T1 {{else}} T0 {{end}}
用于简化if-else,else action可以直接包含另一个if
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
等价于:
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
package main
import (
"html/template"
"os"
)
func comp(a int) bool {
if a == 3 {
return true
}
return false
}
func main() {
t := template.New("test")
t.Funcs(template.FuncMap{"comp": comp})
t, _ = t.Parse(`{{if 1}}
true
{{else}}
false
{{end}}
{{$a := 4}}
{{if $a|comp}}
$a=3
{{else}}
$a!=3
{{end}}`)
t.Execute(os.Stdout, nil)
//output: true
// $a!=3
}
遍历
pipeline的值必须是数组、切片、字典或者通道。如果pipeline的值其长度为0,不会有任何输出;否则dot依次设为数组、切片、字典或者通道的每一个成员元素并执行T1;如果pipeline的值为字典,且键可排序的基本类型,元素也会按键的顺序排序。
{{range pipeline}} T1 {{end}}
pipeline的值必须是数组、切片、字典或者通道。如果pipeline的值其长度为0,不改变dot的值并执行T0;否则会修改dot并执行T1。
{{range pipeline}} T1 {{else}} T0 {{end}}
package main
import (
"text/template"
"os"
)
type User struct {
Contact map[string]string
Num int
}
func main() {
t := template.New("test")
t, _ = t.Parse(`
{{range $k, $v := .Contact}}
{{$k}} {{$v}}
{{end}}
{{range .Contact}}
{{.}}
{{end}}
{{range .Contact}}
{{else}}
{{/* 当 .Pages 为空 或者 长度为 0 时会执行这里 */}}
{{end}}
`)
info := make(map[string]string)
info["qq"] = "656965586"
info["tel"] = "15399265339"
t.Execute(os.Stdout, User{info,100})
//output:
//qq 656965586
//tel 15399265339
//656965586
//15399265339
}
定义局部变量
with 用于重定向 pipeline,创建一个封闭的作用域,在其范围内,可以使用.
action,而与外面的.
无关,只与with的参数有关。如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。
{{with pipeline}} T1 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
{{with pipeline}} T1 {{else}} T0 {{end}}
package main
import (
"text/template"
"os"
)
type User struct {
Contact map[string]string
Num int
}
func main() {
t := template.New("test")
t, _ = t.Parse(`
{{with "hello"}}{{printf "%q" .}}{{end}}
{{with .Contact}}
{{range $k, $v := .}}
{{$k}} {{$v}}
{{end}}
{{end}}
`)
info := make(map[string]string)
info["qq"] = "656965586"
info["tel"] = "15399265339"
t.Execute(os.Stdout, User{info,100})
//output:
//"hello"
//qq 656965586
//tel 15399265339
}
嵌入子模板
每一个模板在创建时都要用一个字符串命名。同时,每一个模板都会和0或多个模板关联,并可以使用它们的名字调用这些模板;这种关联可以传递,并形成一个模板的名字空间。
一个模板可以通过模板调用实例化另一个模板;参见上面的"template" action。name必须是包含模板调用的模板相关联的模板的名字。
执行名为name的模板,提供给模板的参数为nil,如模板不存在输出为""。
嵌入名称为“name”的子模板。使用前,请确保已经用“{{define “name”}}子模板内容{{end}}”定义好了子模板内容。
{{template "name"}}
执行名为name的模板,提供给模板的参数为pipeline的值。将管道的值赋给子模板中的“.”(即“{{.}}”)
{{template "name" pipeline}}
典型用法是定义一组根模板,然后通过重新定义其中的block模板进行自定义。
{{block "name" pipeline}} T1 {{end}}
等价于
{{define "name"}} T1 {{end}}
{{template "name" pipeline}}
当解析模板时,可以定义另一个模板,该模板会和当前解析的模板相关联。模板必须定义在当前模板的最顶层,就像go程序里的全局变量一样。
这种定义模板的语法是将每一个子模板的声明放在"define"和"end" action内部。
define action使用给出的字符串常数给模板命名,举例如下:
`{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}`
它定义了两个模板T1和T2,第三个模板T3在执行时调用这两个模板;最后该模板调用了T3。输出结果是:
ONE TWO
采用这种方法,一个模板只能从属于一个关联。如果需要让一个模板可以被多个关联查找到;模板定义必须多次解析以创建不同的*Template 值,或者必须使用Clone或AddParseTree方法进行拷贝。
可能需要多次调用Parse函数以集合多个相关的模板;参见ParseFiles和ParseGlob函数和方法,它们提供了简便的途径去解析保存在文件中的存在关联的模板。
一个模板可以直接调用或者通过ExecuteTemplate方法调用指定名字的相关联的模板;我们可以这样调用模板:
err := tmpl.Execute(os.Stdout, "no data needed")
if err != nil {
log.Fatalf("execution failed: %s", err)
}
或显式的指定模板的名字来调用:
err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed")
if err != nil {
log.Fatalf("execution failed: %s", err)
}
package main
import (
"os"
"text/template"
)
type User struct {
Id string
UserName string
Age int
}
func test() string {
return "test"
}
func main() {
//嵌套模板
t := template.New("test");
t.Funcs(template.FuncMap{"test": test});
// {{define "模板名"}}模板内容{{end}} 定义模板
// {{template "模板名"}} 引入模板
// {{template "模板名" 函数}} 将函数中的值赋给模板中的{{.}}
t, _ = t.Parse(`
{{/*下面三句模板定义不会有输出*/}}
{{define "tp1"}} 我是模板1 {{end}}
{{define "tp2"}} 我是模板2 {{.}} {{end}}
{{define "tp3"}} {{- template "tp1"}} {{template "tp2"}} {{end}}
{{/*下面三句会有输出*/}}
{{template "tp1"}}
{{template "tp2" test}}
{{template "tp3" test}}
`);
t.Execute(os.Stdout, nil);
//output:
//我是模板1
//我是模板2 test
//我是模板1 我是模板2 <no value>
}
预定义的模板全局函数
执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。
预定义的全局函数如下:
and
函数返回它的第一个empty参数或者最后一个参数;
就是说"and x y"等价于"if x then y else x";所有参数都会执行;
call
执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
其中Y是函数类型的字段或者字典的值,或者其他类似情况;
call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
html
转义文本中的html标签,如将"<"转义为"<",">"转义为">"等。
index
执行结果为第一个参数以剩下的参数为索引/键指向的值;
如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
js
返回用JavaScript的escape处理后的文本。
len
返回它的参数的整数类型长度
not
返回它的单个参数的布尔值的否定
or
返回第一个非empty参数或者最后一个参数;
亦即"or x y"等价于"if x then x else y";所有参数都会执行;
print
即fmt.Sprint
printf
即fmt.Sprintf
println
即fmt.Sprintln
urlquery
返回适合在URL查询中嵌入到形参中的文本转义值。(类似于PHP的urlencode)
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
eq 如果arg1 == arg2则返回真
ne 如果arg1 != arg2则返回真
lt 如果arg1 < arg2则返回真
le 如果arg1 <= arg2则返回真
gt 如果arg1 > arg2则返回真
ge 如果arg1 >= arg2则返回真
为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,和go的||不一样,不做惰性运算,所有参数都会执行,它会将第一个参数和其余参数依次比较,
{{eq arg1 arg2 arg3 arg4}}
即只能作如下比较:
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
比较函数只适用于基本类型(或重定义的基本类型,如"type Celsius float32")。它们实现了go语言规则的值的比较,但具体的类型和大小会忽略掉,因此任意类型有符号整数值都可以互相比较;任意类型无符号整数值都可以互相比较;等等。但是,整数和浮点数不能互相比较。
package main
import (
"text/template"
"os"
)
type User struct {
Contact map[string]string
}
func sum() func(nums ...int) (int, error) {
return func(nums ...int) (int, error) {
sum := 0
for _, v := range nums {
sum += v
}
return sum, nil
};
}
func main() {
//内置的模板函数
t := template.New("test")
t.Funcs(template.FuncMap{"sum": sum})
t, _ = t.Parse(`
//如果3为真,返回4,否则返回3
{{and 3 4}}
//call后第一个参数的返回值必须是一个函数
{{call sum 1 3 5 7}}
//转义文本中的html标签
{{"<br>"|html}}
//返回Contact索引为qq的值
{{index .Contact "qq"}}
//返回用js的escape处理后的文本
{{"?a=123&b=你好"|js}}
//返回参数的长度值
{{"hello"|len}}
//返回单一参数的布尔否定值
{{not 0}}
//如果3为真,返回3,否则返回4
{{or 3 4}}
//fmt.Sprint的别名
{{"你好"|print "世界"}}
//fmt.Sprintf的别名
{{"你好"|printf "%d %s" 123}}
//fmt.Sprintln的别名
{{"你好"|println "世界"}}
//url中get参数转义
{{"?q=关键字&p=1"|urlquery}}
//等于
{{if eq 1 1}}1=1{{end}}
//不等于
{{if ne 1 2}}1!=1{{end}}
//小于
{{if lt 1 3}}1<3{{end}}
//小于等于
{{if le 3 3}}3<=3{{end}}
//大于
{{if gt 3 1}}3>1{{end}}
//大于等于
{{if ge 3 3}}3>=3{{end}}
`)
info := make(map[string]string)
info["qq"] = "656965586"
info["tel"] = "15399265339"
t.Execute(os.Stdout, User{info})
//output:
//如果3为真,返回4,否则返回3
//4
//call后第一个参数的返回值必须是一个函数
//16
//转义文本中的html标签
//<br>
//返回Contact索引为qq的值
//656965586
//回用js的escape处理后的文本
//?a=123&b=你好
//返回参数的长度值
//5
//返回单一参数的布尔否定值
//true
//如果3为真,返回3,否则返回4
//3
//fmt.Sprint的别名
//世界你好
//fmt.Sprintf的别名
//123 你好
//fmt.Sprintln的别名
//世界 你好
//url中get参数转义
//%3Fq%3D%E5%85%B3%E9%94%AE%E5%AD%97%26p%3D1
//等于
//1=1
//不等于
//1!=1
//小于
//1<3
//小于等于
//3<=3
//大于
//3>1
//大于等于
//3>=3
}
自定义模板函数
package main
import (
"os"
"text/template"
)
type User struct {
Id string
UserName string
Age int
}
func (u User)Test(msg string) string {
return msg
}
func test1() string {
return "test1"
}
func test2(msg string) string {
return msg + " ==>"
}
func main() {
//模板函数
t1 := template.New("test1")
//注册模板函数
t1.Funcs(template.FuncMap{"test1": test1})
t1.Funcs(template.FuncMap{"test2": test2})
// {{函数名}}输出函数返回值
// {{函数名 参数1 参数2}}
// {{.字段名|函数名}} 以字段的值作为函数的参数
t1, _ = t1.Parse(`
{{test1}}
{{test2 "test2"}}
{{.Test "2016-01-02 15:04:05"}}
`)
t1.Execute(os.Stdout, User{"1234", "wyy", 22})
//output:
//test1
//test2 ==>
//2016-01-02 15:04:05
}
例子
下面是一些单行模板,展示了pipeline和变量。所有都生成加引号的单词"output":
{{"\"output\""}}
字符串常量
{{`"output"`}}
原始字符串常量
{{printf "%q" "output"}}
函数调用
{{"output" | printf "%q"}}
函数调用,最后一个参数来自前一个command的返回值
{{printf "%q" (print "out" "put")}}
括号内的参数
{{"put" | printf "%s%s" "out" | printf "%q"}}
玩出花的管道的链式调用
{{"output" | printf "%s" | printf "%q"}}
管道的链式调用
{{with "output"}}{{printf "%q" .}}{{end}}
使用dot的with action
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
创建并使用变量的with action
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
将变量使用在另一个action的with action
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
以管道形式将变量使用在另一个action的with action
内容转义
在以下情况,模板标签输出的内容会被转义:
- 在HTML代码里输出HTML代码时,HTML代码会被转义;
- 在Javascript代码里输出Javascript代码时,Javascript代码会被转义;
- 在CSS代码里输出CSS代码时,CSS代码会被转义
有时候,我们并不需要golang的模板引擎在这些地方进行转义,这时候应该怎么办呢?
我们只需要将要输出的内容转为相应的template.HTML、template.JS、template.CSS数据类型就可以避免转义了。这里的template是指html/template包。