模板引擎是 Web 编程中必不可少的一个组件。模板能分离逻辑和数据,使得逻辑简洁清晰,而且模板可复用。引用第二篇文章《程序结构》一文中的图示,咱们能够看到模板引擎在 Web 程序结构中的位置:html
模板引擎按照功能能够划分为两种类型:git
这两类模板引擎都比较极端。无逻辑模板引擎须要在处理器中额外添加不少逻辑用于生成替换的文本。而嵌入逻辑模板引擎则在模板中混入了大量逻辑,致使维护性较差。实用的模板引擎通常介于这二者之间。github
在Go 语言中,text/template
和html/template
两个库实现模板功能。golang
模板内容能够是 UTF-8 编码的任何内容。其中用{{
和}}
包围的部分称为动做,{{}}
外的其它文本在输出保持不变。模板须要应用到数据,模板中的动做会根据数据生成响应的内容来替换。web
模板解析以后能够屡次执行,也能够并行执行,可是注意使用同一个Writer
会致使输出交替出现。编程
模板的内容较多,我将分为两篇文章介绍。本文介绍text/template
,包括 Go 模板的基本概念,用法和注意点。下篇文章介绍html/template
。数组
使用模板引擎通常有 3 个步骤:bash
text/template
或html/template
中的方法解析);package main
import (
"log"
"os"
"text/template"
)
type User struct {
Name string
Age int
}
func stringLiteralTemplate() {
s := "My name is {{ .Name }}. I am {{ .Age }} years old.\n"
t, err := template.New("test").Parse(s)
if err != nil {
log.Fatal("Parse string literal template error:", err)
}
u := User{Name: "darjun", Age: 28}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute string literal template error:", err)
}
}
func fileTemplate() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse file template error:", err)
}
u := User{Name: "dj", Age: 18}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute file template error:", err)
}
}
func main() {
stringLiteralTemplate()
fileTemplate()
}
复制代码
在可执行程序目录中新建模板文件test
,并写入下面的内容:编程语言
My name is {{ .Name }}. I am {{ .Age }} years old.
复制代码
首先调用template.New
建立一个模板,参数为模板名。函数
而后调用Template
类型的Parse
方法,解析模板字符串,生成模板主体。这个方法返回两个值。若是模板语法正确,则返回模板对象自己和一个 nil 值。 若是有语法错误,则返回一个 error 类型的值做为第二个返回值,这时不该该使用第一个返回值。
最后,调用模板对象的Execute
方法,传入参数。Execute
执行模板中的动做,将结果输出到os.Stdout
,即标准输出。最终咱们看到模板中{{ .Name }}
被u
的Name
字段替换,{{ .Age }}
被u
的Age
字段替换,标准输出中显示下面一行字符串:
My name is darjun. I am 28 years old.
复制代码
上面代码中,fileTemplate
函数还演示了如何从文件中加载模板。其中template.ParseFiles
方法会建立一个模板,并将用户指定的模板文件名用做这个新模板的名字:
t, err := template.ParseFiles("test")
复制代码
至关于:
t := template.New("test")
t, err := t.ParseFiles("test")
复制代码
Go 模板中的动做就是一些嵌入在模板里面的命令。动做大致上能够分为如下几种类型:
在介绍其它的动做以前,咱们先看一个很重要的动做,点动做({{ . }}
)。它其实表明是传递给模板的数据,其余动做或函数基本上都是对这个数据进行处理,以此来达到格式化和内容展现的目的。
对前面的代码示例稍做修改:
func main() {
s := "The user is {{ . }}."
t, err := template.New("test").Parse(s)
if err != nil {
log.Fatal("Parse error:", err)
}
u := User{Name: "darjun", Age: 28}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
运行程序,标准输出显示:
The user is {darjun 28}.
复制代码
实际上,{{ . }}
会被替换为传给给模板的数据的字符串表示。这个字符串与以数据为参数调用fmt.Sprint
函数获得的内容相同。咱们能够为User
结构编写一个方法:
func (u User) String() string {
return fmt.Sprintf("(name:%s age:%d)", u.Name, u.Age)
}
复制代码
这样替换的字符串就是格式化以后的内容了:
The user is (name:darjun age:28).
复制代码
注意:为了使用的方便和灵活,在模板中不一样的上下文内,.
的含义可能会改变,下面在介绍不一样的动做时会进行说明。
在介绍动做的语法时,我采用 Go 标准库中的写法。我以为这样写更严谨。 其中pipeline
表示管道,后面会有详细的介绍,如今能够将它理解为一个值。 T1/T2
等形式表示语句块,里面能够嵌套其它类型的动做。最简单的语句块就是不包含任何动做的字符串。
条件动做的语法与编程语言中的if
语句语法相似,有几种形式:
形式一:
{{ if pipeline }} T1 {{ end }}
复制代码
若是管道计算出来的值不为空,执行T1
。不然,不生成输出。下面都表示空值:
false
、0、空指针或接口;形式二:
{{ if pipeline }} T1 {{ else }} T2 {{ end }}
复制代码
若是管道计算出来的值不为空,执行T1
。不然,执行T2
。
形式三:
{{ if pipeline1 }} T1 {{ else if pipeline2 }} T2 {{ else }} T3 {{ end }}
复制代码
若是管道pipeline1
计算出来的值不为空,则执行T1
。反之若是管道pipeline2
的值不为空,执行T2
。若是都为空,执行T3
。
举个栗子:
type AgeInfo struct {
Age int
GreaterThan60 bool
GreaterThan40 bool
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
rand.Seed(time.Now().Unix())
age := rand.Intn(100)
info := AgeInfo {
Age: age,
GreaterThan60: age > 60,
GreaterThan40: age > 40,
}
err = t.Execute(os.Stdout, info)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
在可执行程序的目录下新建模板文件test
,键入下面的内容:
Your age is: {{ .Age }}
{{ if .GreaterThan60 }}
Old People!
{{ else if .GreaterThan40 }}
Middle Aged!
{{ else }}
Young!
{{ end }}
复制代码
运行程序,会随机一个年龄,而后根据年龄区间选择性输出Old People!/Middle Age!/Young!
其中一个。下面是我运行两次运行的输出:
Your age is: 7
Young!
复制代码
Your age is: 79
Old People!
复制代码
这个程序有一个问题,会有多余的空格!咱们以前说过,除了动做以外的任何文本都会原样保持,包括空格和换行!针对这个问题,有两种解决方案。第一种方案是删除多余的空格和换行,test
文件修改成:
Your age is: {{ .Age }}
{{ if .GreaterThan60 }}Old People!{{ else if .GreaterThan40 }}Middle Aged!{{ else }}Young!{{ end }}
复制代码
显然,这个方法会致使模板内容很难阅读,不够理想。为此,Go 提供了针对空白符的处理。若是一个动做以{{-
(注意有一个空格),那么该动做与它前面相邻的非空文本或动做间的空白符将会被所有删除。相似地,若是一个动做以-}}
结尾,那么该动做与它后面相邻的非空文本或动做间的空白符将会被所有删除。例如:
{{23 -}} < {{- 45}}
复制代码
将会生成输出:
23<45
复制代码
回到咱们的例子中,咱们能够将test
文件稍做修改:
Your age is: {{ .Age }}
{{ if .GreaterThan60 -}}
"Old People!"
{{- else if .GreaterThan40 -}}
"Middle Aged!"
{{- else -}}
"Young!"
{{- end }}
复制代码
这样,输出的文本就不会包含多余的空格了。
迭代其实与编程语言中的循环遍历相似。有两种形式:
形式一:
{{ range pipeline }} T1 {{ end }}
复制代码
管道的值类型必须是数组、切片、map、channel。若是值的长度为 0,那么无输出。不然,.
被设置为当前遍历到的元素,而后执行T1
,即在T1
中.
表示遍历的当前元素,而非传给模板的参数。若是值是 map 类型,且键是可比较的基本类型,元素将会以键的顺序访问。
形式二:
{{ range pipeline }} T1 {{ else }} T2 {{ end }}
复制代码
与前一种形式基本同样,若是值的长度为 0,那么执行T2
。
举个栗子:
type Item struct {
Name string
Price int
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
items := []Item {
{ "iPhone", 5499 },
{ "iPad", 6331 },
{ "iWatch", 1499 },
{ "MacBook", 8250 },
}
err = t.Execute(os.Stdout, items)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
在可执行程序目录下新建模板文件test
,键入内容:
Apple Products:
{{ range . }}
{{ .Name }}: ¥{{ .Price }}
{{ else }}
No Products!!!
{{ end }}
复制代码
运行程序,获得下面的输出:
Apple Products:
iPhone: ¥5499
iPad: ¥6331
iWatch: ¥1499
MacBook: ¥8250
复制代码
在range
语句循环体内,.
被设置为当前遍历的元素,能够直接使用{{ .Name }}
或{{ .Price }}
访问产品名称和价格。在程序中,将nil
传给Execute
方法会获得下面的输出:
Apple Products:
No Products!!!
复制代码
设置动做使用with
关键字重定义.
。在with
语句内,.
会被定义为指定的值。通常用在结构嵌套很深时,能起到简化代码的做用。
形式一:
{{ with pipeline }} T1 {{ end }}
复制代码
若是管道值不为空,则将.
设置为pipeline
的值,而后执行T1
。不然,不生成输出。
形式二:
{{ with pipeline }} T1 {{ else }} T2 {{ end }}
复制代码
与前一种形式的不一样之处在于当管道值为空时,不改变.
执行T2
。举个栗子:
type User struct {
Name string
Age int
}
type Pet struct {
Name string
Age int
Owner User
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
p := Pet {
Name: "Orange",
Age: 2,
Owner: User {
Name: "dj",
Age: 28,
},
}
err = t.Execute(os.Stdout, p)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
模板文件内容:
Pet Info:
Name: {{ .Name }}
Age: {{ .Age }}
Owner:
{{ with .Owner }}
Name: {{ .Name }}
Age: {{ .Age }}
{{ end }}
复制代码
运行程序,获得下面的输出:
Pet Info:
Name: Orange
Age: 2
Owner:
Name: dj
Age: 28
复制代码
可见,在with
语句内,.
被替换成了Owner
字段的值。
包含动做能够在一个模板中嵌入另外一个模板,方便模板的复用。
形式一:
{{ template "name" }}
复制代码
形式二:
{{ template "name" pipeline }}
复制代码
其中name
表示嵌入的模板名称。第一种形式,将使用nil
做为传入内嵌模板的参数。第二种形式,管道pipeline
的值将会做为参数传给内嵌的模板。举个栗子:
package main
import (
"log"
"os"
"text/template"
)
func main() {
t, err := template.ParseFiles("test1", "test2")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, "test data")
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
ParseFiles
方法接收可变参数,可将任意多个文件名传给该方法。
模板test1
:
This is in test1.
{{ template "test2" }}
{{ template "test2" . }}
复制代码
模板test2
:
This is in test2.
Get: {{ . }}.
复制代码
运行程序获得输出:
This is in test1.
This is in test2.
Get: <no value>.
This is in test2.
Get: test data.
复制代码
前一个嵌入模板,没有传递参数。后一个传入.
,即传给test1
模板的参数。
在介绍了几种动做以后,咱们回过头来看几种基本组成部分。
注释只有一种语法:
{{ /* 注释 */ }}
复制代码
注释的内容不会呈如今输出中,它就像代码注释同样,是为了让模板更易读。
一个参数就是模板中的一个值。它的取值有多种:
Execute
方法执行终止,返回该错误给调用者;上面几种形式能够结合使用:
{{ .Field1.Key1.Method1.Field2.Key2.Method2 }
复制代码
其实,咱们已经用过不少次参数了。下面看一个方法调用的栗子:
type User struct {
FirstName string
LastName string
}
func (u User) FullName() string {
return u.FirstName + " " + u.LastName
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, User{FirstName: "lee", LastName: "darjun"})
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
模板文件test
:
My full name is {{ .FullName }}.
复制代码
模板执行会使用FullName
方法的返回值替换{{ .FullName }}
,输出:
My full name is lee darjun.
复制代码
关于参数的几个要点:
{{ .Method1 }}
,若是Method1
方法返回一个函数,那么返回值函数不会调用。若是要调用它,使用内置的call
函数。管道的语法与 Linux 中的管道相似,即命令的链式序列:
{{ p1 | p2 | p3 }}
复制代码
每一个单独的命令(即p1/p2/p3...
)能够是下面三种类型:
在一个链式管道中,每一个命令的结果会做为下一个命令的最后一个参数。最后一个命令的结果做为整个管道的值。
管道必须只返回一个值,或者只返回一个值和一个错误。若是返回了非空的错误,那么Execute
方法执行终止,并将该错误返回给调用者。
在迭代程序的基础上稍做修改:
type Item struct {
Name string
Price float64
Num int
}
func (item Item) Total() float64 {
return item.Price * float64(item.Num)
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
item := Item {"iPhone", 5499.99, 2 }
err = t.Execute(os.Stdout, item)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
模板文件test
:
Product: {{ .Name }}
Price: ¥{{ .Price }}
Num: {{ .Num }}
Total: ¥{{ .Total | printf "%.2f" }}
复制代码
先调用Item.Total
方法计算商品总价,而后使用printf
格式化,保留两位小数。最终输出:
Product: iPhone
Price: ¥5499.99
Num: 2
Total: ¥10999.98
复制代码
printf
是 Go 模板内置的函数,这样的函数还有不少。
在动做中,能够用管道的值定义一个变量。
$variable := pipeline
复制代码
$variable
为变量名,声明变量的动做不生成输出。
相似地,变量也能够从新赋值:
$variable = pipeline
复制代码
在range
动做中能够定义两个变量:
range $index, $element := range pipeline
复制代码
这样就能够在循环中经过$index
和$element
访问索引和元素了。
变量的做用域持续到定义它的控制结构的{{ end }}
动做。若是没有这样的控制结构,则持续到模板结束。模板调用不继承变量。
执行开始时,$
被设置为传入的数据参数,即.
的值。
Go 模板提供了大量的预约义函数,若是有特殊需求也能够实现自定义函数。模板执行时,遇到函数调用,先从模板自定义函数表中查找,然后查找全局函数表。
预约义函数分为如下几类:
and/or/not
;call
;print/printf/println
,与用参数直接调用fmt.Sprint/Sprintf/Sprintln
获得的内容相同;eq/ne/lt/le/gt/ge
。在上面条件动做的示例代码中,咱们在代码中计算出大小关系再传入模板,这样比较繁琐,能够直接使用比较运算简化。
有两点须要注意:
默认状况下,模板中无自定义函数,可使用模板的Funcs
方法添加。下面咱们实现一个格式化日期的自定义函数:
package main
import (
"log"
"os"
"text/template"
"time"
)
func formatDate(t time.Time) string {
return t.Format("2016-01-02")
}
func main() {
funcMap := template.FuncMap {
"fdate": formatDate,
}
t := template.New("test").Funcs(funcMap)
t, err := t.ParseFiles("test")
if err != nil {
log.Fatal("Parse errr:", err)
}
err = t.Execute(os.Stdout, time.Now())
if err != nil {
log.Fatal("Exeute error:", err)
}
}
复制代码
模板文件test
:
Today is {{ . | fdate }}.
复制代码
模板的Func
方法接受一个template.FuncMap
类型变量,键为函数名,值为实际定义的函数。 能够一次设置多个自定义函数。自定义函数要求只返回一个值,或者返回一个值和一个错误。 设置以后就能够在模板中使用fdate
了,输出:
Today is 7016-01-07.
复制代码
这里不能使用template.ParseFiles
,由于在解析模板文件的时候fdate
未定义会致使解析失败。必须先建立模板,调用Funcs
设置自定义函数,而后再解析模板。
咱们前面学习了两种模板的建立方式:
template.New
建立模板,而后使用Parse/ParseFiles
解析模板内容;template.ParseFiles
建立并解析模板文件。第一种方式,调用template.New
建立模板时须要传入一个模板名字,后续调用ParseFiles
能够传入一个或多个文件,这些文件中必须有一个基础名(即去掉路径部分)与模板名相同。若是没有文件名与模板名相同,则Execute
调用失败,返回错误。例如:
package main
import (
"log"
"os"
"text/template"
)
func main() {
t := template.New("test")
t, err := t.ParseFiles("test1")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, nil)
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
上面代码先建立模板test
,而后解析文件test1
。执行该程序会出现下面的错误:
Execute error:template: test: "test" is an incomplete or empty template
复制代码
Why?
咱们先来看看模板的结构:
// src/text/template.go
type common struct {
tmpl map[string]*Template // Map from name to defined templates.
option option
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}
复制代码
模板结构Template
中有一个字段common
,common
中又有一个字段tmpl
保存名字到模板的映射。其实,最外层的Template
结构是主模板,咱们调用Execute
方法时执行的就是主模板。 执行ParseFiles
方法时,每一个文件都会生成一个模板。只有文件基础名与模板名相同时,该文件的内容才会解析到主模板中。这也是上面的程序执行失败的缘由——主模板为空。 其它文件解析生成关联模板,存储在字段tmpl
中。关联模板能够是在主模板中经过{{ define }}
动做定义,或者在非主模板文件中定义。关联模板也能够执行,可是须要使用ExecuteTemplate
方法,显式传入模板名:
func main()
t := template.New("test")
t, err := t.ParseFiles("test1")
if err != nil {
log.Fatal("in associatedTemplate Parse error:", err)
}
err = t.ExecuteTemplate(os.Stdout, "test1", nil)
if err != nil {
log.Fatal("in associatedTemplate Execute error:", err)
}
}
复制代码
第二种方式将建立和解析两步合并在一块儿了。template.ParseFiles
方法将传入的第一个文件名做为模板名称,其他的文件(若是有的话)解析后存放在tmpl
中。
t, err := template.ParseFiles("file1", "file2", "file3")
复制代码
其实就等价于:
t := template.New("file1")
t, err := t.ParseFiles("file1", "file2", "file3")
复制代码
少了不一致的可能性,因此调用Execute
方法时不会出现上面的错误。
还有一种建立方式,使用ParseGlob
函数。ParseGlob
会对匹配给定模式的全部文件进行语法分析。
func main() {
t, err := template.ParseGlob("tmpl*.glob")
if err != nil {
log.Fatal("in globTemplate parse error:", err)
}
err = t.Execute(os.Stdout, nil)
if err != nil {
log.Fatal(err)
}
for i := 1; i <= 3; i++ {
err = t.ExecuteTemplate(os.Stdout, fmt.Sprintf("tmpl%d.glob", i), nil)
if err != nil {
log.Fatal(err)
}
}
}
复制代码
ParseGlob
返回的模板以匹配的第一个文件基础名做为名称。ParseGlob
解析时会对同一个目录下的文件进行排序,因此第一个文件老是固定的。
咱们建立三个模板文件,tmpl1.glob
:
In glob template file1.
复制代码
tmpl2.glob
:
In glob template file2.
复制代码
tmpl3.glob
:
In glob template file3.
复制代码
最终输出为:
In glob template file1.
In glob template file1.
In glob template file2.
In glob template file3.
复制代码
注意,若是多个不一样路径下的文件名相同,那么后解析的会覆盖以前的。
在一个模板文件中还能够经过{{ define }}
动做定义其它的模板,这些模板就是嵌套模板。模板定义必须在模板内容的最顶层,像 Go 程序中的全局变量同样。
嵌套模板通常用于布局(layout)。不少文本的结构其实很是固定,例如邮件有标题和正文,网页有首部、正文和尾部等。 咱们能够为这些固定结构的每部分定义一个模板。
定义模板文件layout.tmpl
:
{{ define "layout" }}
This is body.
{{ template "content" . }}
{{ end }}
{{ define "content" }}
This is {{ . }} content.
{{ end }}
复制代码
上面定义了两个模板layout
和content
,layout
中使用了content
。执行这种方式定义的模板必须使用ExecuteTemplate
方法:
func main() {
t, err := template.ParseFiles("layout.tmpl")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.ExecuteTemplate(os.Stdout, "layout", "amazing")
if err != nil {
log.Fatal("Execute error:", err)
}
}
复制代码
嵌套模板在网页布局中应用很是普遍,下一篇文章介绍html/template
时还会讲到。
块动做其实就是定义一个默认模板,语法以下:
{{ block "name" arg }}
T1
{{ end }}
复制代码
其实它就等价于定义一个模板,而后当即使用它:
{{ define "name" }}
T1
{{ end }}
{{ template "name" arg }}
复制代码
若是后面定义了模板content
,那么使用后面的定义,不然使用默认模板。
例如上面的示例中,咱们将模板修改以下:
{{ define "layout" }}
This is body.
{{ block "content" . }}
This is default content.
{{ end }}
{{ end }}
复制代码
去掉后面的content
模板定义,执行layout
时,content
部分会显示默认值。
本文介绍了 Go 提供的模板text/template
。模板比较简单易用,对于一些细节须要多加留意。代码在Github上。