宠物出去
这是文章《如何在Go中使用模板》的第3部分(共14部分)。
. . .
var tmplFile = "pets.tmpl"
tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, dogs)
if err != nil {
panic(err)
}
}
在这段代码中,我们使用Template.New创建一个新的模板,然后调用ParseFiles方法解析模板文件。检查错误后,我们调用新模板的Execute方法,将os.Stdout作为参数传入以将生成的报告打印到终端,同时传入了狗数组。对于第一个参数,可以传入任何实现了io.Writer接口的对象,这意味着我们可以将报告写入文件,例如。我们稍后将展示如何完成这一操作。
完整的程序如下所示:
package main
import (
"os"
"text/template"
)
type Pet struct {
Name string
Sex string
Intact bool
Age string
Breed string
}
func main() {
dogs := []Pet{
{
Name: "Jujube",
Sex: "Female",
Intact: false,
Age: "10 months",
Breed: "German Shepherd/Pitbull",
},
{
Name: "Zephyr",
Sex: "Male",
Intact: true,
Age: "13 years, 3 months",
Breed: "German Shepherd/Border Collie",
},
}
var tmplFile = "pets.tmpl"
tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, dogs)
if err != nil {
panic(err)
}
}
保存程序,然后使用”go run”命令运行它。
- go run pets.go
输出
这是文章《如何在Go中使用模板》的第4部分(共14部分)。
内容片段:
这里还没有内容。
程序还没有打印您的数据,但至少代码执行得很干净。现在让我们写一个模板。
第四步 — 编写模板
一个模板仅仅是UTF-8纯文本,但它并不止于此。是的,它包含一些静态文本,最终输出时将保持不变,但它还包含动作,这些动作是给模板引擎的指令,告诉它如何遍历传入的数据,并决定输出中要包含什么内容。动作被包裹在一对双大括号中—{{ <动作> }}—它们通过光标来访问数据,光标用一个点(.)表示。
传入模板的数据可以是任何东西,但通常传入切片、数组或映射类型——可迭代的东西。让我们让模板依次遍历你的狗切片。
循环遍历切片
在Go代码中,您可以在for循环的开头语句中使用range来迭代一个切片。在模板中,您可以使用range操作实现相同的目的,但是它的语法有所不同:没有for关键字,但是有一个额外的end来关闭循环。
打开 pets.tmpl 文件,并将其内容替换为以下内容:
宠物模板
{{ range . }}
---
(宠物将出现在这里...)
{{ end }}
这里的range动作接受一个参数:光标(.),它指的是整个dogs切片。循环以底部的{{ end }}闭合。在循环体内,你打印了一些静态文本,但还没有涉及到dogs。
保存pets.tmpl文件并重新运行pets.go。
- go run pets.go
输出
— (宠物将出现在这里…) — (宠物将出现在这里…)
由于切片中有两只狗,因此静态文本打印两次。现在让我们将其替换为一些更有用的静态文本,以及狗的数据。
展示一个字段
在这个模板中,当将 . 传递给 range 时,点表示整个切片,但在 range 循环的每次迭代中,点表示切片中的当前项。这使您只需使用裸点即可访问每个宠物的导出字段,而无需引用切片索引。
显示一个字段就像将它用花括号括起来,并在前面加上一个点一样简单。打开pets.tmpl,并用以下内容替换其中的内容:
宠物模板
{{ range . }}
---
Name: {{ .Name }}
Sex: {{ .Sex }}
Age: {{ .Age }}
Breed: {{ .Breed }}
{{ end }}
现在,pets.go将输出每只狗的四个字段中的五个,包括一些字段的标签。(我们稍后解释第五个字段。)
保存并重新运行程序。
- go run pets.go
输出
— Name: Jujube Sex: Female Age: 10 months Breed: German Shepherd/Pitbull — Name: Zephyr Sex: Male Age: 13 years, 3 months Breed: German Shepherd/Border Collie
看起来不错。现在让我们看看如何使用一些条件逻辑来显示第五个字段。
使用条件语句
我们没有在模板中使用{{ .Intact }}来包含”Intact”这个字段的原因是这样不太方便读者。想象一下,如果你的兽医账单中总结你的狗是Intact: false的话。虽然把这个字段存储为布尔值而不是字符串可能更高效,而且Intact是一个适用于性别中性的好名字,但我们可以通过if-else操作在最终报告中以不同的方式显示它。
再次打开pets.tmpl文件,然后将此处突出显示的部分添加进去。
宠物模板
{{ range . }}
---
Name: {{ .Name }}
Sex: {{ .Sex }} ({{ if .Intact }}未绝育{{ else }}已绝育{{ end }})
Age: {{ .Age }}
Breed: {{ .Breed }}
{{ end }}
模板现在检查Intact字段是否为真,并且如果是真,则打印(未绝育),如果不是,则打印(已绝育)。但是我们可以做得更好。让我们进一步编辑模板,以便打印已绝育或已阉割的性别特定术语,而不是笼统的”已绝育”。在最初的else部分中添加一个嵌套的if语句:
宠物模板
{{ range . }}
---
Name: {{ .Name }}
Sex: {{ .Sex }} ({{ if .Intact }}未绝育{{ else }}{{ if (eq .Sex "Female") }}已摘除卵巢{{ else }}已去势{{ end }}{{ end }})
Age: {{ .Age }}
Breed: {{ .Breed }}
{{ end }}
保存模板并运行pets.go
- go run pets.go
输出
— Name: Jujube Sex: Female (已摘除卵巢) Age: 10 months Breed: German Shepherd/Pitbull — Name: Zephyr Sex: Male (未绝育) Age: 13 years, 3 months Breed: German Shepherd/Border Collie
我们有两只狗,但是对于显示绝育状态的情况有三种可能。让我们在pets.go中加入一只狗,以涵盖所有三种情况。编辑pets.go并将第三只狗追加到切片中。
宠物数据
. . .
func main() {
dogs := []Pet{
{
Name: "Jujube",
Sex: "Female",
Intact: false,
Age: "10 months",
Breed: "German Shepherd/Pitbull",
},
{
Name: "Zephyr",
Sex: "Male",
Intact: true,
Age: "13 years, 3 months",
Breed: "German Shepherd/Border Collie",
},
{
Name: "Bruce Wayne",
Sex: "Male",
Intact: false,
Age: "3 years, 8 months",
Breed: "Chihuahua",
},
}
. . .
现在保存并运行pets.go文件。
- go run pets.go
输出
--- 姓名: Jujube 性别: 雌性 (已绝育) 年龄: 10个月 品种: 德国牧羊犬/比特犬 --- 姓名: Zephyr 性别: 雄性 (未绝育) 年龄: 13年3个月 品种: 德国牧羊犬/边境牧羊犬 --- 姓名: Bruce Wayne 性别: 雄性 (已绝育) 年龄: 3年8个月 品种: 吉娃娃
很好,输出结果与预期一致。
现在让我们讨论一下模板函数,就像你刚刚使用的eq函数一样。
使用模板函数
除了eq之外,还有其他用于比较字段值并返回布尔值的函数:gt(大于)、ne(不等于)、le(小于等于)等。您可以通过两种方式调用这些函数和任何模板函数:
-
- 以函数名字开头,之后是一个或多个参数,每个参数之间用空格分开。这就是你在上面使用eq的方式:eq .Sex “Female”。
- 首先写一个参数,之后是一个竖线(|),然后是函数名字,然后是更多的参数。这与Unix命令行上的命令管道类似,就像在命令行上一样,你可以将许多函数调用链接在一起形成一个流水线,将一个调用的输出作为下一个调用的输入,依此类推。
所以虽然你的模板中的eq比较是以eq .Sex “Female”的形式书写的,也可以写成.Sex | eq “Female”。这两个表达式是等价的。
让我们使用len函数在报告的顶部显示狗的数量。打开pets.tmpl文件,将以下内容添加到顶部:
宠物模版
狗的数量: {{ . | len -}}
{{ range . }}
. . .
你也可以写成{{ len . -}}
。
请注意双大括号闭合处的破折号(-)。这样可以防止动作后打印换行符(\n)。您也可以在开头的双大括号前添加破折号({{-
),以避免动作前的换行符。
保存模板并运行pets.go。
- go run pets.go
输出
狗的数量: 3 — 名称: Jujube 性别: 女性(已绝育) 年龄: 10个月 品种: 德国牧羊犬 & 比特犬 — 名称: Zephyr 性别: 雄性(未绝育) 年龄: 13年3个月 品种: 德国牧羊犬 & 边境牧羊犬 — 名称: Bruce Wayne 性别: 雄性(已绝育) 年龄: 3年8个月 品种: 吉娃娃
由于{{. | len -}}
中的连字符,”狗的数量”标签和第一只狗之间没有空行。
你可能已经注意到了,在 text/template 文档中内置函数的列表相当小。好消息是,只要一个 Go 函数返回单个值,或者如果第二个值是错误类型,你就可以让它在模板中可用。
在模板中使用Go函数
假设你想要写一个模板,接收一个狗的切片并只打印最后一个。在模板中,你可以使用内置函数slice来获取切片的子集,它的用法类似于Go语言的mySlice[x:y]。你可以写{{ slice . 2 }}
来获取一个有三个元素的切片的最后一个,尽管slice函数返回的是另一个切片而不是一个元素。也就是说,{{ slice . 2 }}
等同于slice[2:],而不是slice[2]。(该函数还可以接受多个索引,例如{{ slice . 0 2 }}
表示获取切片slice[0:2],但在这里不会使用。)
但是在模板中,如何引用切片的最后一个索引呢?虽然可以使用len函数,但切片的最后一个元素索引是len-1,而不幸的是,在模板中不能进行数学运算。
这就是自定义函数的用武之地。让我们编写一个减少整数的函数,并将该函数提供给我们的模板使用。
但在此之前,让我们创建一个新的模板。打开一个名为lastPet.tmpl的新文件,并粘贴以下内容:
最后一个宠物模板
{{- range (len . | dec | slice . ) }}
---
姓名:{{ .Name }}
性别:{{ .Sex }} ({{ if .Intact }}完整{{ else }}{{ if ("Female" | eq .Sex) }}已绝育(母){{ else }}已绝育(公){{ end }}{{ end }})
年龄:{{ .Age }}
品种:{{ .Breed }}
{{ end -}}
在第一行对dec
函数的调用是指你将要定义并传递给模板的自定义函数。在pets.go
的main()
函数中,在dogs切片的下面和tmpl.Execute()
的调用之前进行以下更改(已标出)。
宠物信息展示
这是文章《如何在Go中使用模板》的第8部分(共14部分)。
. . .
funcMap := template.FuncMap{
"dec": func(i int) int { return i - 1 },
}
var tmplFile = "lastPet.tmpl"
tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
if err != nil {
panic(err)
}
. . .
首先,你正在声明一个FuncMap
,它是一个函数映射:键是模板中可用的函数名称,值是函数本身。这里你有一个函数,dec
,是一个匿名函数,在此提供是因为它非常简短。它接受一个整数,从中减去一,并返回结果。
然后你正在更改模板文件名称。最后,在调用ParseFiles
之前,你将在调用之前插入对Template.Funcs
的调用,并将刚刚定义的funcMap
传递给它。在调用ParseFiles
之前必须调用Funcs
方法。
在运行代码之前,让我们了解一下模板中的范围操作所发生的情况。
最后一个宠物的模板:
{{- range (len . | dec | slice . ) }}
你正在获取你的狗的切片长度,将其传递给你的自定义减法函数减去一,然后将其作为第二个参数传递给前面讨论过的切片函数。所以对于一个三只狗的切片,range动作相当于{{- range (slice . 2) }}
。
保存 pets.go 并运行它。
go run pets.go
输出:
--- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua
看起来不错。如果你想展示最后两只狗而不只是最后一只,那该怎么办?编辑 lastPet.tmpl 文件,并在管道中再次调用 dec。
最后的宠物模板:
{{- range (len . | dec | dec | slice . ) }}
. . .
保存文件并再次运行pets.go。
go run pets.go
输出:
--- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd/Border Collie --- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua
你可以想象一下如何通过给dec函数传递一个参数并更改其名称来改进它,这样你就可以调用减2而不是dec | dec。
现在假设你想要以不同于斜杠的方式展示像Zephyr这样的混血狗。你不需要编写自己的函数来做到这一点,因为strings包中已经有一个函数可以实现这个目的,并且你可以从任何包中借用一个函数来在你的模板中使用。编辑pets.go文件,引入strings包并将其函数之一添加到funcMap中。
宠物出行
第9部分:在Go模板中使用自定义函数
这是文章《如何在Go中使用模板》的第9部分(共14部分)。
package main
import (
"os"
"strings"
"text/template"
)
. . .
func main() {
. . .
funcMap := template.FuncMap{
"dec": func(i int) int { return i - 1 },
"replace": strings.ReplaceAll,
}
. . .
}
您正在导入strings
包,并将其ReplaceAll
函数添加到您的funcMap
中,名称为replace
。然后,请编辑lastPet.tmpl
以使用此函数。
最后一只宠物模板 (zuì hòu yī zhī chǒng wù mó bǎn)
在这个模板中,您可以使用{{replace .OldString .NewString}}
的语法来调用我们刚刚注册的替换函数。
这是文章《如何在Go中使用模板》的第10部分(共14部分)。
{{- range (len . | dec | dec | slice . ) }}
---
姓名:{{ .Name }}
性别:{{ .Sex }} ({{ if .Intact }}未绝育{{ else }}{{ if ("雌性" | eq .Sex) }}已绝育(母){{ else }}已绝育(公){{ end }}{{ end }})
年龄:{{ .Age }}
品种:{{ replace .Breed "/" " & " }}
{{ end -}}
保存文件并再次运行它。
- go run pets.go
输出
— 姓名:Zephyr 性别:雄性 (未绝育) 年龄:13岁3个月 品种:德国牧羊犬和边境牧羊犬 — 姓名:Bruce Wayne 性别:雄性 (已绝育(公)) 年龄:3岁8个月 品种:吉娃娃
盈风的品种现在不再使用斜杠,而是使用一个和符号。
您本可以在 pets.go 中操纵该字符串,而不是在模板中操纵,但数据的展现是模板的工作,而不是代码的工作。
实际上,一些狗的数据中已经包含了一些展示内容,也许不应该这样。品种字段将多个品种压缩成一个字符串,并用斜杠标点分隔它们。这种单字符串模式可能会导致数据输入员在数据库中引入不同的格式:拉布拉多/贵宾、拉布拉多和贵宾、拉布拉多、贵宾、拉布拉多贵宾混合等等。为了避免这种格式的歧义,更灵活地按品种进行搜索和更容易地展示它,最好将品种存储为一个字符串切片 ([]string) 而不是字符串。然后你可以在模板中使用 strings.Join 函数来打印所有品种,再加上 .Breed 字段的额外注释(纯种或混合品种)。
注意
自己尝试
尝试修改你的代码和模板来实现这些更改。完成后,点击下方的”解决方案”来检查你的工作。
解决方案
pets.go
. . .
type Pet struct {
Name string
Sex string
Intact bool
Age string
Breed []string
}
func main() {
dogs := []Pet{
{
Name: "Jujube",
. . .
Breed: []string{"German Shepherd", "Pit Bull"},
},
{
Name: "Zephyr",
. . .
Breed: []string{"German Shepherd", "Border Collie"},
},
{
Name: "Bruce Wayne",
. . .
Breed: []string{"Chihuahua"},
},
}
funcMap := template.FuncMap{
"dec": func(i int) int { return i - 1 },
"replace": strings.ReplaceAll,
"join": strings.Join,
}
. . .
} // end main
lastPet.tmpl
{{- range (len . | dec | dec | slice . ) }}
---
名字: {{ .Name }}
性别: {{ .Sex }} ({{ if .Intact }}未绝育{{ else }}{{ if ("女性" | eq .Sex) }}绝育{{ else }}中性化{{ end }}{{ end }})
年龄: {{ .Age }}
品种: {{ join .Breed "和" }} ({{ if len .Breed | eq 1 }}纯种{{ else }}混种{{ end }})
{{ end -}}
最后,让我们将相同的狗的数据渲染到一个HTML文档中,看看为什么在模板的输出是HTML时,你应该始终使用html/template包。
步骤5 — 编写HTML模板
一个命令行工具可以使用text/template来整洁地打印输出,而其他一些批处理程序可以使用它从某些数据创建结构良好的文件。但Go模板通常用于渲染Web应用程序的HTML页面。例如,流行的开源静态网站生成器Hugo同时使用text/template和html/template作为其模板的基础。
HTML使用角括号来包含元素(<div>
),使用和号来标记实体(
),以及使用引号来包裹标签属性(<a href="https://www.digitalocean.com/">
)。如果您的模板插入的任何数据包含这些字符,使用text/template包可能会导致HTML格式不正确,或者更糟糕的是,代码注入。
html/template包的作用不同。这个包会转义那些有问题的字符,将它们替换成安全的HTML等价物(实体)。你数据中的&符号会变成&,左尖括号会变成<,以此类推。
让我们使用之前的相同狗狗数据来创建一个HTML文件,但首先我们将继续使用text/template来显示其危险性。
打开pets.go文件,并将下面的文本添加到枣子的姓名字段中:
. . .
dogs := []Pet{
{
Name: "<script>alert(\"Gotcha!\");</script>Jujube",
Sex: "Female",
Intact: false,
Age: "10 months",
Breed: "German Shepherd/Pit Bull",
},
{
Name: "Zephyr",
Sex: "Male",
Intact: true,
Age: "13 years, 3 months",
Breed: "German Shepherd/Border Collie",
},
{
Name: "Bruce Wayne",
Sex: "Male",
Intact: false,
Age: "3 years, 8 months",
Breed: "Chihuahua",
},
}
. . .
现在在一个名为petsHtml.tmpl的新文件中创建一个HTML模板。
宠物网页模板 (petsHtml.tmpl)
<p><strong>宠物:</strong> {{ . | len }}</p>
{{ range . }}
<hr />
<dl>
<dt>姓名</dt>
<dd>{{ .Name }}</dd>
<dt>性别</dt>
<dd>{{ .Sex }} ({{ if .Intact }}完整{{ else }}{{ if (eq .Sex "Female") }}已绝育(母){{ else }}已绝育(公){{ end }}{{ end }})</dd>
<dt>年龄</dt>
<dd>{{ .Age }}</dd>
<dt>品种</dt>
<dd>{{ replace .Breed "/" " & " }}</dd>
</dl>
{{ end }}
保存HTML模板。在运行pets.go之前,我们需要编辑tmpFile变量,但让我们同时编辑程序,将模板输出到文件而不是终端。打开pets.go并在main()函数中添加突出显示的代码。
宠物信息输出
. . .
funcMap := template.FuncMap{
"dec": func(i int) int { return i - 1 },
"replace": strings.ReplaceAll,
}
var tmplFile = "petsHtml.tmpl"
tmpl, err = template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
if err != nil {
panic(err)
}
var f *os.File
f, err = os.Create("pets.html")
if err != nil {
panic(err)
}
err = tmpl.Execute(f, dogs)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}
您正在打开一个名为pets.html的新文件,将其传递给tmpl.Execute(而不是os.Stdout),然后在完成后关闭文件。
现在运行”go run pets.go”来生成HTML文件。然后,在您的浏览器中打开这个本地网页。
浏览器已运行注入的脚本。这就是为什么您永远不应该使用text/template软件包来生成HTML,尤其是当您无法完全信任模板数据的来源时。
除了在数据中转义HTML字符外,html/template包的使用方式与text/template完全相同,并且具有相同的基本名称(template)。这意味着要使您的模板注入安全,您只需将text/template导入替换为html/template即可。编辑pets.go文件并立即执行此操作。
宠物外出
package main
import (
"os"
"strings"
"html/template"
)
. . .
保存文件并最后一次运行,覆盖pets.html。然后在浏览器中刷新HTML文件。

html/template包将注入的脚本渲染为网页中的纯文本。在您的文本编辑器中打开pets.html(或在浏览器中查看页面源代码),并查看第一只狗狗菜菜。
宠物页面
. . .
<dl>
<dt>名称</dt>
<dd><script>alert("Gotcha!");</script>菜菜</dd>
<dt>性别</dt>
<dd>雌性(已绝育)</dd>
<dt>年龄</dt>
<dd>10个月</dd>
<dt>品种</dt>
<dd>德国牧羊犬 & 比特犬</dd>
</dl>
. . .
HTML在菜菜的名字中替换了尖括号和引号字符,并且还替换了品种中的&符号。
结论
Go模板是一个方便的工具,可以将任何文本包装在任何数据周围。您可以在命令行工具中使用它们来格式化输出,在您的Web应用程序中使用它们来渲染HTML等等。在本教程中,您使用了Go语言的内置模板包,可以从相同的数据中打印出格式良好的文本和HTML页面。要了解如何使用这些包的更多信息,请查看text/template和html/template的文档。