参考《Go 语言权威指南》中第 23 章:HTML 和文本模板,内容同步 go 1.24.1
加载多个模板
一种方式是多次调用 template.ParseFiles()
,另外一种方式将多个文件加载到一个 template.ParseFiles()
中:
go
func main() {
t1, err1 := template.ParseFiles("templates/template.html")
t2, err2 := template.ParseFiles("templates/extras.html")
if err1 == nil && err2 == nil {
t1.Execute(os.Stdout, &Kayak)
os.Stdout.WriteString("\n")
t2.Execute(os.Stdout, nil)
} else {
Printfln("Error: %v %v", err1.Error(), err2.Error())
}
}
go
func main() {
allTemplates, err := template.ParseFiles("templates/template.html", "templates/extras.html")
if err == nil {
allTemplates.ExecuteTemplate(os.Stdout, "template.html", &Kayak)
os.Stdout.WriteString("\n")
allTemplates.ExecuteTemplate(os.Stdout, "extras.html", &Kayak)
} else {
log.Printf("Error: %v", err.Error())
}
}
go
func main() {
allTemplates, err := template.ParseGlob("templates/*.html")
if err == nil {
for _, t := range allTemplates.Templates() {
Printfln("Template name: %v", t.Name())
}
} else {
log.Printf("Error: %v", err.Error())
}
}
go
func main() {
allTemplates, err := template.ParseGlob("templates/*.html")
if err == nil {
selectedTemplate := allTemplates.Lookup("template.html")
selectedTemplate.Execute(os.Stdout, &Kayak)
} else {
log.Printf("Error: %v", err.Error())
}
}
模板动作
动作 | 描述 |
---|---|
{{ /* comment */ }} |
代码注释 |
{{ value }} 或 {{ expr }} |
将数据值或表达式结果插入模板 |
{{ value.fieldName }} {{ $x.fieldName }} |
插入结构字段的值,其中 $x 为模板中定义的变量 |
{{ value.method arg }} |
调用方法并将结果插入模板输出。不使用括号,参数用空格分隔 |
{{ func arg }} |
调用一个函数并将结果插入模板输出 |
`{{ expr | value.method }} {{ expr |
{{ range value }} ... {{ end }} |
遍历指定的数据(array , slice , map , iter.Seq , iter.Seq2 , integer 或 channel ),并为每个内容添加 range 和 end 关键词之间的内容 |
{{ range value }} ... {{ else }} ... {{ end }} |
类似于前面的 range/end 组合,但是定义了一个额外的嵌套内容,如果切片不包含元素,则使用该部分 |
{{ break }} |
提前结束循环,不执行后续迭代 |
{{ continue }} |
结束当前循环,继续执行后续迭代 |
{{ if expr }} ... {{ end }} |
对表达式求值,如果结果为 true 则执行嵌套的模板内容。该动作可以与可选的 else 和 else if 子句一起使用 |
{{ with expr }} ... {{ end }} |
如果表达式结果不是 nil 或空字符串,则该动作将计算表达式并执行嵌套模板内容。该动作可以与可选子句一起使用 |
{{ with expr }} ... {{ else }} ... {{ end }} |
如果 expr 不为空则执行第一个语句,否则执行 else 中的内容。 |
{{ with expr }} ... {{ else with expr2 }} ... {{ end }} |
为了简化 with-else 链的外观,使用 else 动作 with的可以直接包括另一个 with ; {with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}} |
{{ define "name" }} ... {{ end }} |
该动作定义了一个具有指定名称的模板 |
{{ template "name" expr }} |
该动作使用指定的名称和数据执行模板,并在输出中插入结果 |
{{ block "name" expr }} ... {{ end }} |
该动作用指定的名称定义一个模板,并用指定的数据调用它。这通常用于定义一个可以被另一个文件加载的模板替换的模板。实际上是以下定义的便捷方式: 1. 使用 {{ define "name" }} T1 {{ end }} 定义模板 2. 使用 {{ template "name" value }} 执行模板 |
html
{{"\"output\""}}
A string constant.
{{`"output"`}}
A raw string constant.
{{printf "%q" "output"}}
A function call.
{{"output" | printf "%q"}}
A function call whose final argument comes from the previous
command.
{{printf "%q" (print "out" "put")}}
A parenthesized argument.
{{"put" | printf "%s%s" "out" | printf "%q"}}
A more elaborate call.
{{"output" | printf "%s" | printf "%q"}}
A longer chain.
{{with "output"}}{{printf "%q" .}}{{end}}
A with action using dot.
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
A with action that creates and uses a variable.
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
A with action that uses the variable in another action.
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
The same, but pipelined.
格式化数据值
函数 | 描述 |
---|---|
print |
这是 fmt.Sprint 函数的别名 |
printf |
这是 fmt.Sprintf 函数的别名 |
println |
这是 fmt.Sprintln 函数的别名 |
html |
这个函数将一个值安全编码为 HTML 文档 |
js |
这个函数将一个值安全编码为 JavaScript 文档 |
urlquery |
这个函数对一个值进行解码,用于 URL 查询字符串 |
下面是一些使用示例:
html
<h1>Price {{ printf "$%.2f" .Price }}</h1>
<p>{{ html "<b>This is bold text</b>" }}</p>
链接和括号模板表达式
链接表达式为多个值创建一个流水线,允许将一个方法或函数的输出用作另一个方法或函数的输入。
html
<h1>Discount Price: {{ .ApplyDiscount 10 | printf "$%.2f" }}</h1>
<!-- 另一种传递参数方式:使用括号 -->
<h1>Discount Price: {{ printf "$%.2f" (.ApplyDiscount 10) }}</h1>
修剪空白字符
默认情况下,模板的内容完全按照文件中的定义呈现,包括动作之间的空格。
html
<h1>
Name: {{ .Name }}, Category: {{ .Category }}, Price,
{{ printf "%.2f" .Price }}
</h1>
减号( -
)可以用来修剪这些空格,应用在开始或结束动作的大括号之前或之后。
diff
<h1>
Name: {{ .Name }}, Category: {{ .Category }}, Price,
+ {{- printf "%.2f" .Price -}}
</h1>
html
<h1>
Name: Kayak, Category: Watersports, Price,279.00</h1>
执行后我们会发现 h1
标签后面的空格被去除了,这是因为刚好位于动作之后,但是 h1
前面的空格并未被去除。要解决这个问题,我们可以在输出中插入一个空字符串的动作来修剪空白:
diff
<h1>
{{- "" -}} Name: {{ .Name }}, Category: {{ .Category }}, Price,
{{- printf "%.2f" .Price -}}
</h1>
在模板中使用切片
html
{{ range . -}}
<h1>Name: {{ .Name }}, Category: {{ .Category }}, Price:
{{- printf "$%.2f" .Price -}}
</h1>
{{ end }}
内置的切片函数
函数 | 描述 |
---|---|
slice |
该函数创建一个新的切片。它的参数是原始切片、起始索引和结束索引 |
index |
该函数返回指定索引处的元素 |
len |
该函数返回指定切片的长度 |
举例: |
slice x 1 2
同x[1:2]
slice x
同x[:]
slice x 1
同x[1:]
slice x 1 2 3
同x[1:2:3]
html
<h1>There are {{ len . }} products in the source data.</h1>
<h1>First product: {{ index . 0 }}</h1>
{{ range slice . 3 5 -}}
<h1>Name: {{ .Name }}, Category: {{ .Category }}, Price:
{{- printf "$%.2f" .Price -}}
</h1>
{{ end }}
使用条件判断
函数 | 描述 |
---|---|
eq arg1 arg2 |
如果 arg1 == arg2 ,则该函数返回 true |
ne arg1 arg2 |
如果 arg1 != arg2 ,则该函数返回 true |
lt arg1 arg2 |
如果 arg1 < arg2 ,则该函数返回 true |
le arg1 arg2 |
如果 arg1 <= arg2 ,则该函数返回 true |
gt arg1 arg2 |
如果 arg1 > arg2 ,则该函数返回 true |
ge arg1 arg2 |
如果 arg1 >= arg2 ,则该函数返回 true |
and arg1 arg2 |
如果 arg1 和 arg2 都是 true ,则该函数返回 true |
not arg1 |
如果 arg1 是 false ,则该函数返回 true |
html
<h1>There are {{ len . }} products in the source data.</h1>
<h1>First product: {{ index . 0 }}</h1>
{{ range . -}}
{{ if lt .Price 100.00 -}}
<h1>Name: {{ .Name }}, Category: {{ .Category }}, Price:
{{- printf "$%.2f" .Price -}}
</h1>
{{ else if gt .Price 1500.00 -}}
<h1>Expensive Product {{ .Name }} ({{- printf "$%.2f" .Price -}})</h1>
{{ else -}}
<h1>Midrange Product {{ .Name }} ({{- printf "$%.2f" .Price -}})</h1>
{{ end -}}
{{ end }}
创建命名嵌套模板
define
关键字用于创建一个可以通过名字执行的嵌套模板,它允许内容被指定一次,并与 template
动作一起重复使用。
html
{{ define "currency" }}{{ printf "$%.2f" . }}{{ end }}
{{ define "basicProduct" -}}
Name: {{.Name }}, Category: {{.Category }}, Price: {{- template "currency" .Price }}
{{- end }}
{{ define "expensiveProduct" -}}
Expensive Product {{.Name }} ({{- template "currency" .Price }})
{{- end }}
<h1>There are {{ len . }} products in the source data.</h1>
<h1>First product: {{ index . 0 }}</h1>
{{ range . -}}
{{ if lt .Price 100.00 -}}
{{ template "basicProduct" . }}
{{ else if gt .Price 1500.00 -}}
{{ template "expensiveProduct" . }}
{{ else -}}
<h1>Midrange Product {{ .Name }} ({{- printf "$%.2f" .Price -}})</h1>
{{ end -}}
{{ end }}
ad-attention
嵌套的命名模板会加剧空格的问题,因为模板周围的空格会包含在主模板的输出中。
要解决这个空格问题,我们可以对主模板内容使用 define
和 end
关键字排除用于分隔其他命名模板的空格:
html
{{ define "currency" }}{{ printf "$%.2f" . }}{{ end }}
{{ define "basicProduct" -}}
Name: {{.Name }}, Category: {{.Category }}, Price: {{- template "currency" .Price }}
{{- end }}
{{ define "expensiveProduct" -}}
Expensive Product {{.Name }} ({{- template "currency" .Price }})
{{- end }}
{{- define "mainTemplate" }}
<h1>There are {{ len . }} products in the source data.</h1>
<h1>First product: {{ index . 0 }}</h1>
{{ range . -}}
{{ if lt .Price 100.00 -}}
{{ template "basicProduct" . }}
{{ else if gt .Price 1500.00 -}}
{{ template "expensiveProduct" . }}
{{ else -}}
<h1>Midrange Product {{ .Name }} ({{- printf "$%.2f" .Price -}})</h1>
{{ end -}}
{{ end }}
{{- end }}
在使用时将原来加载整个模板的 allTemplates.Lookup("template.html")
改成 allTemplates.Lookup("mainTemplate")
即可。
定义模板块
模板块用于定义具有默认内容的模板,它可以在另一个模板文件中被覆盖,这需要同时加载和执行多个模板。
html
{{ define "mainTemplate" -}}
<h1>This is the layout header</h1>
{{- block "body" . }}
<h2>There are {{ len . }} products in the source data.</h2>
{{ end -}}
<h1>This is the layout footer</h1>
{{ end }}
单独使用时,模板文件的输出包括块中的内容。但是这个内容可以由另外一个模板文件重新定义。
go
{{ define "body" }}
{{- range. }}
<h2>Product {{ .Name }} ({{ printf "$%.2f" .Price }})</h2>
{{- end }}
{{ end }}
这些模板必须被按顺序加载,饮食 block
动作的文件应当先于包含重新定义模板的 define
动作的文件被加载。
go
package main
import (
"os"
"html/template"
)
func Exec(t *template.Template) error {
return t.Execute(os.Stdout, Products)
}
func main() {
allTemplates, err := template.ParseFiles("templates/template.html", "templates/list.html")
if err == nil {
selectedTemplated := allTemplates.Lookup("mainTemplate")
err = Exec(selectedTemplated)
}
if err != nil {
Printfln("Error: %v %v", err.Error())
}
}
定义模板函数
通过特定于 Template
的自定义函数来补充内置函数的功能不足以满足开发需求。
go
package main
import (
"os"
"html/template"
)
// 获取所有产品的分类
func GetCategories(products []Product) (categories []string) {
catMap := map[string]string{}
for _, p := range products {
if catMap[p.Category] == "" {
catMap[p.Category] = p.Category
categories = append(categories, p.Category)
}
}
return
}
func Exec(t *template.Template) error {
return t.Execute(os.Stdout, Products)
}
func main() {
allTemplates := template.New("allTemplates")
allTemplates.Funcs(map[string]interface{}{
"getCats": GetCategories,
})
allTemplates, err := allTemplates.ParseGlob("templates/*.html")
if err == nil {
selectedTemplated := allTemplates.Lookup("mainTemplate")
err = Exec(selectedTemplated)
}
if err != nil {
Printfln("Error: %v %v", err.Error())
}
}
html
{{ define "mainTemplate" -}}
<h1>There ar {{ len . }} products in the source data.</h1>
{{ range getCats . -}}
<h1>Category: {{ . }}</h1>
{{ end }}
{{ end }}
禁用函数结果编码
在Go语言的html/template
包中,默认情况下会对输出内容进行HTML转义,以防止XSS(跨站脚本攻击)。
diff
package main
import (
"html/template"
"os"
)
// 获取所有产品的分类
func GetCategories(products []Product) (categories []string) {
catMap := map[string]string{}
for _, p := range products {
if catMap[p.Category] == "" {
catMap[p.Category] = p.Category
- categories = append(categories, p.Category)
+ categories = append(categories, "<b>p.Category</b>")
}
}
return
}
func Exec(t *template.Template) error {
return t.Execute(os.Stdout, Products)
}
func main() {
allTemplates := template.New("allTemplates")
allTemplates.Funcs(map[string]interface{}{
"getCats": GetCategories,
})
allTemplates, err := allTemplates.ParseGlob("templates/*.html")
if err == nil {
selectedTemplated := allTemplates.Lookup("mainTemplate")
err = Exec(selectedTemplated)
}
if err != nil {
Printfln("Error: %v %v", err.Error())
}
}
结果中某项数据结果:<h1>Category: <b>p.Category</b></h1>
如果不想对模板内容进行转义,可以使用 html/template
包定义的一组 string
类型别名,用于表示函数的结果需要特殊处理:
diff
...
-func GetCategories(products []Product) (categories []string) {
+func GetCategories(products []Product) (categories []template.HTML) {
...
ad-attention
在《Go 语言权威指南》中只是将上面代码中返回值 `categories` 由 `[]string` 修改成了 `[]template.HTML`,但是输出结果还是转义了,需要将第二个 `append` 传值修改为 `append(categories, template.HTML("<b>p.Category</b>"))` 才符合期望。
下面是用于表示类型的类型别名:
类型别名 | 描述 |
---|---|
CSS |
表示 CSS 内容 |
HTML |
表示 HTML 的一个片段 |
HTMLAttr |
表示将用作 HTML 属性的值 |
JS |
表示 JavaScript 的代码片段 |
JSStr |
表示 JavaScript 表达式中引号之间的值 |
Srcset |
表示可以在 img 元素的 srcset 属性中使用的值 |
URL |
表示一个 URL |
定义模板变量
在模板中我们可以使用 $variable
来定义变量:
{{ $lang := "go" }}
:定义一个当前模板全局变量lang
{{ $lang = "python" }}
:变量重新赋值{{ range $i, $v := . }}
:遍历循环,可以循环体中使用