《Go 语言权威指南》学习笔记:HTML 和文本模板

150 阅读5分钟

参考《Go 语言权威指南》中第 23 章:HTML 和文本模板,内容同步 go 1.24.1

加载多个模板

一种方式是多次调用 template.ParseFiles(),另外一种方式将多个文件加载到一个 template.ParseFiles() 中:

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())
	}
}
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())
	}
}

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())
	}
}
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 | func }}
使用竖线将表达式链接在一起,这样第一个表达式的结果将用作第二个表达式的最后一个参数
{{ range value }}
...
{{ end }}
遍历指定的数据(array, slice, map, iter.Seq, iter.Seq2, integerchannel),并为每个内容添加 rangeend 关键词之间的内容
{{ range value }}
...
{{ else }}
...
{{ end }}
类似于前面的 range/end 组合,但是定义了一个额外的嵌套内容,如果切片不包含元素,则使用该部分
{{ break }}提前结束循环,不执行后续迭代
{{ continue }}结束当前循环,继续执行后续迭代
{{ if expr }}
...
{{ end }}
对表达式求值,如果结果为 true 则执行嵌套的模板内容。该动作可以与可选的 elseelse 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 }} 执行模板
{{"\"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 查询字符串

下面是一些使用示例:

<h1>Price {{ printf "$%.2f" .Price }}</h1>
<p>{{ html "<b>This is bold text</b>" }}</p>

链接和括号模板表达式

链接表达式为多个值创建一个流水线,允许将一个方法或函数的输出用作另一个方法或函数的输入。

<h1>Discount Price: {{ .ApplyDiscount 10 | printf "$%.2f" }}</h1>
<!-- 另一种传递参数方式:使用括号 -->
<h1>Discount Price: {{ printf "$%.2f" (.ApplyDiscount 10) }}</h1>

修剪空白字符

默认情况下,模板的内容完全按照文件中的定义呈现,包括动作之间的空格。

<h1>
  Name: {{ .Name }}, Category: {{ .Category }}, Price,
    {{ printf "%.2f" .Price }}
</h1>

减号( - )可以用来修剪这些空格,应用在开始或结束动作的大括号之前或之后。

<h1>
  Name: {{ .Name }}, Category: {{ .Category }}, Price,
+   {{- printf "%.2f" .Price -}}
</h1>
<h1>
  Name: Kayak, Category: Watersports, Price,279.00</h1>

执行后我们会发现 h1 标签后面的空格被去除了,这是因为刚好位于动作之后,但是 h1 前面的空格并未被去除。要解决这个问题,我们可以在输出中插入一个空字符串的动作来修剪空白:

<h1>
  {{- "" -}} Name: {{ .Name }}, Category: {{ .Category }}, Price,
    {{- printf "%.2f" .Price -}}
</h1>

在模板中使用切片

{{ range . -}}
  <h1>Name: {{ .Name }}, Category: {{ .Category }}, Price: 
    {{- printf "$%.2f" .Price -}}
  </h1>
{{ end }}

内置的切片函数

函数描述
slice该函数创建一个新的切片。它的参数是原始切片、起始索引和结束索引
index该函数返回指定索引处的元素
len该函数返回指定切片的长度
举例:
  • slice x 1 2x[1:2]
  • slice xx[:]
  • slice x 1x[1:]
  • slice x 1 2 3x[1:2:3]
<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如果 arg1arg2 都是 true,则该函数返回 true
not arg1如果 arg1false,则该函数返回 true
<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 动作一起重复使用。

{{ 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 }}
嵌套的命名模板会加剧空格的问题,因为模板周围的空格会包含在主模板的输出中。

要解决这个空格问题,我们可以对主模板内容使用 defineend 关键字排除用于分隔其他命名模板的空格:

{{ 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") 即可。

定义模板块

模板块用于定义具有默认内容的模板,它可以在另一个模板文件中被覆盖,这需要同时加载和执行多个模板。

{{ 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 }}

单独使用时,模板文件的输出包括块中的内容。但是这个内容可以由另外一个模板文件重新定义。

{{ define "body" }}
  {{- range. }}
    <h2>Product {{ .Name }} ({{ printf "$%.2f" .Price }})</h2>
  {{- end }}
{{ end }}

这些模板必须被按顺序加载,饮食 block 动作的文件应当先于包含重新定义模板的 define 动作的文件被加载。

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 的自定义函数来补充内置函数的功能不足以满足开发需求。

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())
	}
}

{{ define "mainTemplate" -}}
  <h1>There ar {{ len . }} products in the source data.</h1>
  {{ range getCats . -}}
    <h1>Category: {{ . }}</h1>
  {{ end }}
{{ end }}

禁用函数结果编码

在Go语言的html/template包中,默认情况下会对输出内容进行HTML转义,以防止XSS(跨站脚本攻击)。

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: &lt;b&gt;p.Category&lt;/b&gt;</h1>

如果不想对模板内容进行转义,可以使用 html/template 包定义的一组 string 类型别名,用于表示函数的结果需要特殊处理:

...
-func GetCategories(products []Product) (categories []string) {
+func GetCategories(products []Product) (categories []template.HTML) {
...
在《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 := . }}:遍历循环,可以循环体中使用