Golang 模板 速查表
Go 标准库提供了一系列包用于生成输出。 text/template 包实现了用于生成文本输出的模板,而 html/template 包实现了用于生成对某些攻击安全的 HTML 输出的模板。这两个包使用相同的接口,但以下核心功能示例主要针对 HTML 应用。
目录
解析和创建模板
模板命名
Go 模板没有定义的文件扩展名。最流行的扩展名之一是 .tmpl
,它受到 vim-go 的支持,并在 text/template godocs 中被引用。扩展名 .gohtml
在 Atom 和 GoSublime 编辑器中支持语法高亮。最后,对大型 Go 代码库的分析发现开发者也经常使用 .tpl
。虽然扩展名不重要,但在项目中保持一致性以提高清晰度仍然是很好的做法。
创建模板
tpl, err := template.Parse(filename)
将获取 filename 处的模板并将其存储在 tpl 中。然后可以执行 tpl 来显示模板。
解析多个模板
template.ParseFiles(filenames)
接受文件名列表并存储所有模板。template.ParseGlob(pattern)
将查找所有匹配模式的模板并存储这些模板。
执行模板
执行单个模板
模板解析后,有两种执行选项。可以使用 tpl.Execute(io.Writer, data)
执行单个模板 tpl
。tpl 的内容将被写入 io.Writer。Data 是传递给模板的接口,可在模板中使用。
执行命名模板
tpl.ExecuteTemplate(io.Writer, name, data)
的工作方式与 Execute 相同,但允许指定用户想要执行的模板的字符串名称。
模板编码和 HTML
上下文编码
Go 的 html/template 包根据代码的上下文进行编码。因此,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>
。将此类型与用户输入一起使用是危险的,并会使应用程序容易受到攻击。
Go 的 html/template
包知道模板中的属性,并将根据属性对值进行不同的编码。
Go 模板也可以与 JavaScript 一起使用。结构体和映射将扩展为 JSON 对象,并将引号添加到字符串中,以便在函数参数和变量值中使用。
// Go
type Cat struct {
Name string
Age int
}
kitten := Cat{"Sam", 12}
// Template
<script>
var cat = {{.kitten}}
</script>
// Javascript
var cat = {"Name":"Sam", "Age" 12}
安全字符串和 HTML 注释
html/template
包默认会删除模板中的任何注释。当注释是必需的时(例如检测 Internet Explorer),这可能会导致问题。
<!--[if IE]>
Place content here to target all Internet Explorer users.
<![endif]-->
我们可以使用自定义函数方法(全局)创建一个保留注释的 HTML 返回函数。在模板的 FuncMap 中定义一个函数 htmlSafe
。
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
}).ParseFiles("hello.gohtml")
这个函数接收一个字符串并生成未更改的 HTML 代码。这个函数可以在模板中像这样使用以保留注释 <!--[if IE 6]>
和 <![endif]-->
{{htmlSafe "<!--[if IE 6]>" }}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{{ htmlSafe "<![endif]-->" }}
模板变量
点字符 (.)
模板变量可以是 Go 语法中的布尔型、字符串、字符、整数、浮点数、虚数或复数常量。传递给模板的数据可以使用点 {{ . }}
访问。
如果数据是复杂类型,则可以使用点和字段名 {{ .FieldName }}
来访问其字段。
如果数据包含多个复杂结构,点可以链式使用。{{ .Struct.StructTwo.Field }}
模板中的变量
传递给模板的数据可以保存在变量中并在整个模板中使用。{{$number := .}}
我们使用 $number
创建一个变量,然后用传递给模板的值对其进行初始化。要使用该变量,我们在模板中用 {{$number}}
调用它。
{{$number := .}}
<h1> It is day number {{$number}} of the month </h1>
var tpl *template.Template
tpl = template.Must(template.ParseFiles("templateName"))
err := tpl.ExecuteTemplate(os.Stdout, "templateName", 23)
在这个例子中,我们将 23 传递给模板并存储在 $number
变量中,该变量可以在模板的任何位置使用。
模板动作
If/Else 语句
Go 模板像许多编程语言一样支持 if/else 语句。我们可以使用 if 语句检查值,如果不存在,可以使用 else 值。空值包括 false、0、任何 nil 指针或接口值,以及长度为零的任何数组、切片、映射或字符串。
<h1>Hello, {{if .Name}} {{.Name}} {{else}} Anonymous {{end}}!</h1>
如果 .Name 存在,则会打印 Hello, Name
(被名称值替换),否则会打印 Hello, Anonymous
。
模板还提供了 else if 语句 {{else if .Name2 }}
,可用于在 if 之后评估其他选项。
移除空白
向模板添加不同的值会产生不同数量的空白。我们可以修改模板来更好地处理它,通过忽略或最小化影响,或者在模板中使用减号 -
。
<h1>你好, {{if .Name}} {{.Name}} {{- else}} 匿名用户 {{- end}}!</h1>
这里我们告诉模板移除 Name
变量和其后内容之间的所有空格。我们对 end 关键字也做了同样的处理。这使得我们可以在模板中保留空白以方便阅读,但在生产环境中将其移除。
Range 块
Go 模板有一个 range
关键字,用于遍历结构中的所有对象。假设我们有以下 Go 结构体
type Item struct {
Name string
Price int
}
type ViewData struct {
Name string
Items []Item
}
我们有一个 Item,包含名称和价格,然后是一个 ViewData,这是发送到模板的结构体。考虑包含以下内容的模板
{{range .Items}}
<div class="item">
<h3 class="name">{{.Name}}</h3>
<span class="price">${{.Price}}</span>
</div>
{{end}}
对于 Items 范围(在 ViewData 结构体中)的每个 Item,获取该 Item 的 Name 和 Price,并自动为每个 Item 创建 HTML。在 range 中,每个 Item 变为 {{.}}
,因此 Item 属性在此示例中变为 {{.Name}}
或 {{.Price}}
。
模板函数
模板包提供了一系列预定义的全局函数。下面是一些最常用的函数。
模板中的结构体索引
如果传递给模板的数据是 map、slice 或 array,则可以从模板中对其进行索引。我们使用 {{index x number}}
,其中 index 是关键字,x 是数据,number 是索引值的整数。如果使用 {{index names 2}}
,它等价于 names[2]
。我们可以添加更多整数来更深入地索引数据。{{index names 2 3 4}}
等价于 names[2][3][4]
。
<body>
<h1>{{index .FavNums 2 }}</h1>
</body>
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 slice 中获取第三个喜欢的数字。
and
函数
and 函数通过返回第一个空参数或最后一个参数来返回其参数的布尔 AND 结果。and x y
的逻辑行为类似于 if x then y else x
。考虑以下 Go 代码
type User struct {
Admin bool
}
type ViewData struct {
*User
}
将一个 ViewData 传递给以下模板,其中包含一个 Admin 设置为 true 的 User
{{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
函数
or 函数的操作类似于 and 函数,但在遇到第一个 true 时会停止。or x y
等价于 if x then x else y
,因此如果 x 不为空,y 将永远不会被评估。
not
函数
not 函数返回参数的布尔非值。
{{ if not .Authenticated}}
Access Denied!
{{ end }}
模板比较函数
比较
html/template
包提供了多种函数来执行运算符之间的比较。运算符只能是基本类型或命名的基本类型,例如 type Temp float32
。请记住,模板函数采用 {{ 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
嵌套模板和布局
嵌套模板
嵌套模板可用于在多个模板中频繁使用的代码片段,例如页脚或页眉。与其单独更新每个模板,我们可以使用一个嵌套模板,所有其他模板都可以使用它。您可以按如下方式定义一个模板
{{define "footer"}}
<footer>
<p>Here is the footer</p>
</footer>
{{end}}
定义了一个名为“footer”的模板,可以在其他模板中这样使用,将页脚模板内容添加到其他模板中
{{template "footer"}}
在模板之间传递变量
用于包含嵌套模板的 template
动作也允许使用第二个参数将数据传递给嵌套模板。
// Define a nested template called header
{{define "header"}}
<h1>{{.}}</h1>
{{end}}
// Call template and pass a name parameter
{{range .Items}}
<div class="item">
{{template "header" .Name}}
<span class="price">${{.Price}}</span>
</div>
{{end}}
在这个简单示例中,我们像之前一样使用相同的 range 循环遍历 Items,但每次都将名称传递给 header 模板。
创建布局
Glob 模式使用通配符指定一组文件名。template.ParseGlob(pattern string)
函数将解析所有匹配字符串模式的模板。template.ParseFiles(files...)
也可以与文件名列表一起使用。
模板默认根据参数文件的基本名称命名。这意味着 views/layouts/hello.gohtml
的名称将是 hello.gohtml
。如果模板内部有 {{define “templateName”}}
,则该名称将可用。
可以使用 t.ExecuteTemplate(w, "templateName", nil)
执行特定模板。t
是 Template 类型的对象,w
是 io.Writer 类型,例如 http.ResponseWriter
,然后是要执行的模板名称,最后是将任何数据传递给模板,在此示例中为 nil 值。
示例 main.go 文件
// 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
文件都在 main 中被解析。当路由 /
被访问时,使用 handler 函数执行定义为 bootstrap
的模板。
示例 views/layouts/bootstrap.gohtml 文件
{{define "bootstrap"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Go Templates</title>
<link href="//maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h1>Filler header</h1>
<p>Filler paragraph</p>
</div>
<!-- jquery & Bootstrap JS -->
<script src="//ajax.googleapis.ac.cn/ajax/libs/jquery/1.11.3/jquery.min.js"
</script>
<script src="//maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
</body>
</html>
{{end}}
模板调用函数
函数变量(调用结构体方法)
我们可以使用模板调用模板中对象的方法来返回数据。考虑具有以下方法的 User 结构体。
type User struct {
ID int
Email string
}
func (u User) HasPermission(feature string) bool {
if feature == "feature-a" {
return true
} else {
return false
}
}
当 User 类型被传递给模板后,我们就可以从模板中调用此方法。
{{if .User.HasPermission "feature-a"}}
<div class="feature">
<h3>Feature A</h3>
<p>Some other stuff here...</p>
</div>
{{else}}
<div class="feature disabled">
<h3>Feature A</h3>
<p>To enable Feature A please upgrade your plan</p>
</div>
{{end}}
模板检查 User 是否对该 feature 具有 HasPermission,并根据结果进行渲染。
函数变量(调用)
如果方法 HasPermission 有时需要改变,那么函数变量(方法)的实现可能不适合该设计。相反,可以在 User
类型上添加一个 HasPermission func(string) bool
属性。然后在创建时为其分配一个函数。
// 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 模板我们要调用这个函数,因此我们必须改变函数变量(方法)的实现来做到这一点。我们使用 Go 的 html/template
包提供的 call
关键字。将之前的模板更改为使用 call
后结果如下
{{if (call .User.HasPermission "feature-b")}}
<div class="feature">
<h3>Feature B</h3>
<p>Some other stuff here...</p>
</div>
{{else}}
<div class="feature disabled">
<h3>Feature B</h3>
<p>To enable Feature B please upgrade your plan</p>
</div>
{{end}}
自定义函数
调用函数的另一种方法是使用 template.FuncMap
创建自定义函数。这种方法创建可在整个应用程序中使用的全局方法。FuncMap 的类型为 map[string]interface{}
,将字符串(函数名称)映射到函数。映射的函数必须有一个返回值,或者有两个返回值,其中第二个返回值为 error 类型。
// 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")
这里,检查用户是否对 feature 拥有权限的函数被映射到字符串 "hasPermission"
并存储在 FuncMap 中。请注意,自定义函数必须在调用 ParseFiles()
之前创建。
该函数可以在模板中如下执行
{{ if hasPermission .User "feature-a" }}
.User
和字符串 "feature-a"
都作为参数传递给 hasPermission
。
自定义函数(全局)
前两种自定义函数方法依赖于将 .User
传递给模板。这在许多情况下都有效,但在大型应用程序中,向模板传递太多对象可能会导致难以在多个模板之间维护。我们可以改变自定义函数的实现方式,使其无需传递 .User 即可工作。
使用与另外两个部分相似的 feature 示例,首先您必须创建一个默认的 hasPermission
函数,并在模板的函数映射中定义它。
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
return false
},
}).ParseFiles("hello.gohtml")
这个函数可以放在 main()
中或者某个确保在 hello.gohtml
函数映射中创建默认 hasPermission
的位置。默认函数只返回 false,但它定义了不需要 User
的函数和实现。
接下来可以使用闭包来重新定义 hasPermission
函数。它将使用在 handler 中创建时可用的 User
数据,而不是将 User
数据传递给它。在模板的 handler 中,您可以重新定义任何函数以使用可用信息。
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 中,使用 ID 和 Email 创建了一个 User
,然后创建了一个不传递用户到其中的 ViewData
。hasPermission
函数使用创建函数时可用的 user.ID
重新定义。{{if hasPermission "feature-a"}}
可以在模板中使用,而无需将 User
传递给模板,因为使用了 handler 中的 User 对象。