在 Web 开发中,用户看到的页面大部分是静态内容,只有少部分根据用户请求动态生成。例如网站的用户列表页面,不同用户之间只有数据不同,而页面结构保持一致。为了复用这些 "不变的页面结构",我们就需要 模板技术(Template)。
Go 语言自身提供了功能强大的模板引擎,使用 text/template 和 html/template 包即可完成模板处理。它不仅支持变量替换,还支持条件判断、循环、管道(pipeline)、自定义函数、嵌套模板等功能。
本文将从基础到高级,全面讲解 Go 模板的使用方法。
一、什么是模板?
模板是一类"包含静态结构 + 动态插值占位符"的文件。
例如在 JSP 中使用 <%= %>、PHP 中使用 <?php ?>,这些都是通过模板将数据渲染到页面。
在 MVC 模式中:
-
Model:负责数据处理
-
View:负责展示内容(模板系统作用的部分)
-
Controller:负责处理请求、调用 Model、选择 View
Go 的模板引擎就是用来处理 View 层的渲染逻辑。
二、Go 模板的基本使用
Go 使用 template 包加载、解析和渲染模板。
Go
func handler(w http.ResponseWriter, r *http.Request) {
t := template.New("tmpl")
t, _ = t.ParseFiles("tmpl/welcome.html")
user := GetUser()
t.Execute(w, user)
}
-
创建模板对象
-
解析模板文件
-
获取需要渲染的动态数据
-
执行渲染到输出流中
三、字段操作
使用 {{ }} 输出数据
Go 语言的模板通过 {{ }} 来包含需要在渲染时被替换的字段**,{{ . }}** 表示当前的对象
Go
type Person struct {
UserName string
}
func main() {
t := template.New("field")
t, _ = t.Parse("hello {{.UserName}}!")
p := Person{UserName: "Astaxie"}
t.Execute(os.Stdout, p)
}
输出
Go
hello Astaxie!
输出嵌套字段内容
上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用 {{with ...}}...{{end}} 和 {{range ...}}{{end}} 来进行数据的输出。
- {{range}} 这个和 Go 语法里面的 range 类似,循环操作数据
- {{with}} 操作是指当前对象的值,类似上下文的概念
Go
package main
import (
"html/template"
"os"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an email {{.}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
可以在range前加 -和空格(搭配使用)来去除遍历产生的空行
Go
{{- range .Emails}}
an email {{.}}!
{{- end}}
条件处理
在 Go 模板里面如果需要进行条件判断,那么我们可以使用和 Go 语言的 if-else 语法类似的方式来处理,如果 pipeline 为空,那么 if 就认为是 false,下面的例子展示了如何使用 if-else 语法:
Go
package main
import (
"os"
"text/template"
)
func main() {
tEmpty := template.New("template test")
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n"))
tEmpty.Execute(os.Stdout, nil)
tWithValue := template.New("template test")
tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n"))
tWithValue.Execute(os.Stdout, nil)
tIfElse := template.New("template test")
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
tIfElse.Execute(os.Stdout, nil)
}
pipelines
pipe(管道) 是编程中一种核心的 "数据传递机制",本质是连接两个或多个程序 / 进程的 "虚拟通道"------ 让一个程序的输出(stdout,标准输出)直接作为另一个程序的输入(stdin,标准输入),无需中间文件存储,高效实现数据流转。
Go 语言模板最强大的一点就是支持 pipe 数据,在 Go 语言里面任何 {{}} 里面的都是 pipelines 数据
pipelines(流水线,也常译作 "管道线")是 pipe(管道)思想的规模化、体系化延伸 ------ 不再是单个 "管道" 连接两个组件,而是把多个管道、多个处理步骤按固定顺序串联 / 并联起来,形成一套 "端到端" 的自动化数据处理 / 任务执行流程。
模板变量
有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如 withrangeif 过程中申明局部变量,这个变量的作用域是 {{end}} 之前,Go 语言通过申明的局部变量格式如下所示:
Go
$variable := pipeline
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
模板函数
模板在输出对象的字段值时,采用了 fmt 包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把 @ 替换成 at 例如:astaxie at beego.me,如果要实现这样的功能,我们就需要自定义函数来做这个功能。
每一个模板函数都有一个唯一值的名字,然后与一个 Go 函数关联,通过如下的方式来关联
Go
type FuncMap map[string]interface{}
例如,如果我们想要的 email 函数的模板函数名是 emailDeal,它关联的 Go 函数名称是 EmailDealWith, 那么我们可以通过下面的方式来注册这个函数
先注册,接着就可以在模板里调用
Go
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
EmailDealWith 这个函数的参数和返回值定义如下:
func EmailDealWith(args ...interface{}) string
将@替换为at
Go
package main
import (
"fmt"
"html/template"
"log"
"os"
"strings"
)
type Person struct {
Username string
Emails []string
Friends []*Friend
}
type Friend struct {
FName string
}
func main() {
f1 := Friend{"sha"}
f2 := Friend{"er"}
t := template.New("example")
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
t, _ = t.Parse(`
hello {{.Username}}
{{- range .Emails}}
an email {{emailDeal .}}!
{{- end}}
{{- with .Friends }}
{{- range .}}
my friend is {{.FName}}
{{- end}}
{{- end}}
`)
p := Person{
Username: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
err := t.Execute(os.Stdout, p)
if err != nil {
log.Println(err)
return
}
}
func EmailDealWith(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
subStr := strings.Split(s, "@")
if len(subStr) != 2 {
return s
}
return subStr[0] + " at " + subStr[1]
}
Must 操作
模板包里面有一个函数 Must,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。
Go
package main
import (
"fmt"
"text/template"
)
func main() {
tOk := template.New("first")
template.Must(tOk.Parse(" some static text /* and a comment */"))
fmt.Println("The first one parsed OK.")
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
fmt.Println("The second one parsed OK.")
fmt.Println("The next one ought to fail.")
tErr := template.New("check parse error with Must")
template.Must(tErr.Parse(" some static text {{ .Name }"))
}
嵌套模板
我们平常开发 Web 应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成 header、content、footer 三个部分。
Go
--定义
{{define "子模板名称"}}内容{{end}}
--调用
{{template "子模板名称"}}
Go
//header.tmpl
{{define "header"}}
<html>
<head>
<title>演示信息</title>
</head>
<body>
{{end}}
//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>演示嵌套</h1>
<ul>
<li>嵌套使用define定义子模板</li>
<li>调用使用template</li>
</ul>
{{template "footer"}}
{{end}}
//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}
Go
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
s1.ExecuteTemplate(os.Stdout, "header", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "content", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "footer", nil)
fmt.Println()
s1.Execute(os.Stdout, nil)
}
通过上面的例子我们可以看到通过 template.ParseFiles 把所有的嵌套模板全部解析到模板里面,其实每一个定义的 {{define}} 都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似 map 的一种关系 (key 是模板的名称,value 是模板的内容),然后我们通过 ExecuteTemplate 来执行相应的子模板内容,我们可以看到 header、footer 都是相对独立的,都能输出内容,content 中因为嵌套了 header 和 footer 的内容,就会同时输出三个的内容。但是当我们执行 s1.Execute,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。
同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析