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

参考《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, 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 }} 执行模板
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 2x[1:2]
  • slice xx[:]
  • slice x 1x[1:]
  • slice x 1 2 3x[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 如果 arg1arg2 都是 true,则该函数返回 true
not arg1 如果 arg1false,则该函数返回 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 复制代码
嵌套的命名模板会加剧空格的问题,因为模板周围的空格会包含在主模板的输出中。

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

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: &lt;b&gt;p.Category&lt;/b&gt;</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 := . }}:遍历循环,可以循环体中使用
相关推荐
极限实验室41 分钟前
Operator 开发入门系列(一):Hello World
go
纪元A梦13 小时前
华为OD机试真题——跳格子3(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
Bohemian15 小时前
浅谈Golang逃逸分析
后端·面试·go
栩栩云生15 小时前
📥 x-cmd install | Pathos - 告别混乱!你的终端 $PATH 环境变量管理神器
go·命令行
航哥16 小时前
Go语言编译器的正确打开方式(一)- 从源码编译 go
go·编译器
楽码1 天前
一文看懂!编程语言访问变量指针和复制值
后端·go·编程语言
forever231 天前
单个服务添加 OpenTelemetry (otel)
go
来杯咖啡1 天前
Golang 事务消息队列:基于 SQLite 的轻量级消息队列解决方案
后端·go
十分钟空间1 天前
Go语言实现权重抽奖系统
后端·go
DemonAvenger1 天前
Go并发编程进阶:基于Channel的并发控制模式实战指南
分布式·架构·go