在 Go
语言的生态系统中,模板引擎是一个强大且实用的工具。text/template
包是 Go
语言标准库中的一部分,用于生成文本输出,例如 goctl
代码生成、HTML
、XML
或其他格式的文本文件。在这篇文章中,我们将深入探讨 text/template
的语法和功能,帮助你更好地理解和应用。
text/template
官方包:https://pkg.go.dev/text/template
基本概念
text/template
包主要通过定义模板和执行模板来生成动态内容。模板包含了静态文本和动态指令,动态指令通常用双花括号 {{ }}
包围。例如:
package main import ( "os" "text/template" ) func main() { tmpl, err := template.New("example").Parse("Hello, {{.}}!") if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, "World") if err != nil { panic(err) } }
上面的例子中,我们创建了一个简单的模板,包含静态文本 Hello
, 和动态指令 {{.}}
,动态指令将在执行时被替换为传递的参数 World
。
二、模板的基本语法
在 Go
模板中,所有的动作(Action)
由 {{
和 }}
作为边界符,在此之外的可以称做静态文本,静态文本在执行模板渲染时会原封不动的复制输出(部分与 {{
或者 }}
就近的字符可能接受空白符格式化)。模板的渲染,需要用一个共享的 io.Writer
来进行渲染内容输出,因此如果并行执行模板渲染,可能导致内容错乱。
知识点:
- 模板变量接受任何格式的 UTF-8 编码文本
- 静态文本将原封不动复制输出
文本和空格(Text and spaces)
默认情况下,执行模板时,动作 (Action)
之间的所有文本都会被逐字复制,但是,为了帮助格式化模板源代码,模板渲染会按照如下规则进行格式化:
- 规则1:如果一个操作的左分隔符(默认为
{{
)后面紧跟着一个负号和空白,即{{-
则{{
前一个文本中的所有尾部空白都会被删除。 - 规则2:如果右分隔符
}}
前面有空白和减号,即-}}
,则}}
后面紧挨着的文本中所有前导空白都会被裁剪掉。 - 规则3:如果需要做代码模板格式化,空白符号必须存在:
{{- 3}}
与{{3 -}}
类似但会裁剪紧挨着的文本空白符号,而{{-3}}
则解析为包含数字-3
的操作。
如下是一段模板渲染的程序,变量 demo
接收 action1
、action2
两个参数,我们以如下程序来分别测试一下带有符号和空白符的模板渲染结果。
package main import ( "os" "text/template" ) func main() { demo := `<{{.action1}} {{.action2}}!>` tmpl, err := template.New("text_and_spaces").Parse(demo) if err != nil { panic(err) } if err = tmpl.Execute(os.Stdout, map[string]string{ "action1": "Hello", "action2": "World", }); err != nil { panic(err) } }
说明
以下表格中均使用上文程序进行模板渲染,第2列动态替换为代码中
demo
变量,action1
变量固定值为Hello
,action2
固定值为World
。如下表格模板列中,为了便于阅读区分空白符,用一个
·
代表一个空白符,实际代码运行请拷贝模板(源代码版本)列
在 `Go` 语言的生态系统中,模板引擎是一个强大且实用的工具。`text/template` 包是 `Go` 语言标准库中的一部分,用于生成文本输出,例如 `goctl` 代码生成、`HTML`、`XML` 或其他格式的文本文件。在这篇文章中,我们将深入探讨 `text/template` 的语法和功能,帮助你更好地理解和应用。 > `text/template` 官方包:[https://pkg.go.dev/text/template](https://pkg.go.dev/text/template) ## 一、基本概念 `text/template` 包主要通过定义模板和执行模板来生成动态内容。模板包含了静态文本和动态指令,动态指令通常用双花括号 `{{ }}` 包围。例如: ```go package main import ( "os" "text/template" ) func main() { tmpl, err := template.New("example").Parse("Hello, {{.}}!") if (err != nil) { panic(err) } err = tmpl.Execute(os.Stdout, "World") if err != nil { panic(err) } }
上面的例子中,我们创建了一个简单的模板,包含静态文本 Hello
和动态指令 {{.}}
,动态指令将在执行时被替换为传递的参数 World
。
模板的基本语法
在 Go
模板中,所有的动作(Action)
由 {{
和 }}
作为边界符,在此之外的可以称作静态文本,静态文本在执行模板渲染时会原封不动地复制输出(部分与 {{
或者 }}
就近的字符可能接受空白符格式化)。模板的渲染,需要用一个共享的 io.Writer
来进行渲染内容输出,因此如果并行执行模板渲染,可能导致内容错乱。
知识点:
- 模板变量接受任何格式的 UTF-8 编码文本。
- 静态文本将原封不动复制输出。
文本和空格(Text and spaces)
默认情况下,执行模板时,动作 (Action)
之间的所有文本都会被逐字复制,但是,为了帮助格式化模板源代码,模板渲染会按照如下规则进行格式化:
- 规则1:如果一个操作的左分隔符(默认为
{{
)后面紧跟着一个负号和空白,即{{-
,则{{
前一个文本中的所有尾部空白都会被删除。 - 规则2:如果右分隔符
}}
前面有空白和减号,即-}}
,则}}
后面紧挨着的文本中所有前导空白都会被裁剪掉。 - 规则3:如果需要做代码模板格式化,空白符号必须存在:
{{- 3}}
与{{3 -}}
类似但会裁剪紧挨着的文本空白符号,而{{-3}}
则解析为包含数字-3
的操作。
如下是一段模板渲染的程序,变量 demo
接收 action1
、action2
两个参数,我们以如下程序来分别测试一下带有符号和空白符的模板渲染结果。
package main import ( "os" "text/template" ) func main() { demo := `<{{.action1}} {{.action2}}!>` tmpl, err := template.New("text_and_spaces").Parse(demo) if err != nil { panic(err) } if err = tmpl.Execute(os.Stdout, map[string]string{ "action1": "Hello", "action2": "World", }); err != nil { panic(err) } }
说明
以下表格中均使用上文程序进行模板渲染,第2列动态替换为代码中
demo
变量,action1
变量固定值为Hello
,action2
固定值为World
。如下表格模板列中,为了便于阅读区分空白符,用一个
·
代表一个空白符,实际代码运行请拷贝模板(源代码版本)列。
场景 | 模板(源代码版本) | 模板(阅读版本) | 输出结果 | 说明 |
无格式化空白符 | <{{.action1}} - {{.action2}}!> |
<{{.action1}}··-··{{.action2}}!> |
<Hello··-··World!> |
|
规则1 | <{{.action1}} - {{- .action2}}!> |
<{{.action1}}··-··{{-·.action2}}!> |
<Hello··-World!> |
- 右边的2个空白符被裁剪掉了 |
规则2 | <{{.action1 -}} - {{.action2}}!> |
<{{.action1·-}}··-··{{.action2}}!> |
<Hello-··World!> |
- 左边的2个空白符被裁剪掉了 |
规则1&&规则2 | <{{.action1 -}} - {{- .action2}}!> |
<{{.action1·-}}··-··{{-·.action2}}!> |
<Hello-World!> |
- 左右两边的2个空白符都被裁剪掉了 |
对于使用 -
和 white-space
来裁剪 white-space
的格式化操作中,white-space
的定义为空格
、水平制表符 \t
、回车符 \r
和换行符 \n
,即:<{{.action1\t-}}\t- {{.action2}}!>
、<{{.action1\r-}}\r- {{.action2}}!>
、<{{.action1\n-}}\n- {{.action2}}!>
模板输出结果均为 <Hello- World!>
。
注意:white-space在模板格式化规则中不包含
\v
、\f
、0x85
和0xA0
。
动作(Actions)
动作是模板中用于生成动态内容的指令,由 {{
和 }}
包裹,常见动作有:
- 注释动作:
{{/* a comment */}}
注释动作会自动忽略多行注释,包含注释中的空白和换行内容。 - pipeline 动作:
{{pipeline}}
管道的默认动作是将文本值拷贝到输出,类似fmt.Print 做结果输出,当结合条件判断时,其有另外的意义。
例如:在上文代码中,模板 <{{.action1}} - {{.action2}}!>
中包含2个 pipeline 动作 {{.action1}}
和 {{.action2}}
,其没有配合条件判断,因此其在模板渲染时直接当作文本值 Hello
和 World
拷贝了标准输出,但当我们将模板换成 {{if .action1}} - {{.action2}}!{{end}}
则结果输出为 - World!
,这里的 pipeline .action1
就是一个条件表达式了,下文我们会对 pipeline
做详细说明。
注意
在动作中的
pipeline
并非管道的意思,可以通俗的理解为一个模板参数,与下文的管道并非同一个含义。
- if 条件动作:
{{if pipeline}}<Hello World>{{end}}
如果pipeline
的值是非0
值,则模板渲染后输出<Hello World>
,否则没有任何输出,{{if pipeline}}
和{{end}}
必须对应。
知识点:
0 值是指在没有明确初始化的情况下,变量的默认值。
- 整数类型(如
int
、int32
、uint
等):零值为0
- 浮点类型(如
float32
、float64
):零值为0.0
- 布尔类型:零值为
false
- 字符串类型:零值为
""
(空字符串)- 指针类型:零值为
nil
- 切片类型:零值为
length
为0
。- 映射类型:零值为
nil
- 通道类型:零值为
nil
- 接口类型:零值为
nil
- 结构体类型:其所有字段的零值组合
- if-else 动作:
{{if pipeline}}<Hello World>{{else}}<Hi World>{{end}}
如果pipeline的值是非0值,则模板渲染后输出,否则输出。{{if pipeline}}
、{{else}}
和{{end}}
必须对应。 - if-else-if 动作:
{{if pipeline1}}<Hello World>{{else if pipeline2}}<Hi World>{{end}}
如果pipeline1
的值是非0
值,则模板渲染后输出<Hello World>
,否则如果pipeline2
为非0
值,则输出<Hi World>
。 - range 动作:
{{range pipeline}}<Hello World>{{end}}
如果pipeline
为非0
值,则模板渲染后根据循环次数循环输出<Hello World>
。 - range-else 动作:
{{range pipeline}}<Hello World>{{else}}<Hi World>{{end}}
如果pipeline
为非0
值,则模板渲染后根据循环次数循环输出<Hello World>
,否则循环输出<Hi World>
。
知识点:
range
动作中的pipeline
必须为数组、切片、map
或者channel
。
有意思的是,如果 range
的 pipeline
为 map
结构,且 map
的 key
为基础数据类型,则 range
会按照 key
的升序进行循环,如下示例:
package main import ( "os" "text/template" ) func main() { demo := `{{range $key,$value := .action1}}<{{$key}}.{{$value}}>{{end}}` tmpl, err := template.New("text_and_spaces").Parse(demo) if err != nil { panic(err) } if err = tmpl.Execute(os.Stdout, map[string]any{ "action1": map[int]string{ 2: "two", 1: "one", 3: "three", 5: "four", 4: "four", }, "action2": "World", }); err != nil { panic(err) } }
以上 range map
的模板渲染结果输出为 <1.one><2.two><3.three><4.four><5.four>
,这和我们理解的 go map
的遍历不一样。
- break 动作:
{{break}}
{{break}}
动作和for
循环内的break
功能相似,这里的{{break}}
用于提前结束range
动作中循环。 - continue 动作:
{{continue}}
continue
动作和for
循环内的continue
功能相似,这里的{{continue}}
用于提前停止range
动作中当次循环,进入range
动作中下一循环。 - define 动作:
{{define "foo"}}{{end}}
define
动作用于在模板内定义一个子模板,foo
为子模板名称。 - 不带 pipeline 的子模板动作:
{{template "foo"}}
用于引用指定子模板名称为foo
的模板内容,如:
package main import ( "os" "text/template" ) func main() { // 定义包含子模板 foo 的不带参模板 const text = `{{define "foo"}}<Hello,go-zero>{{end}}{{template "foo"}}` tmpl := template.Must(template.New("main").Parse(text)) // 执行模板 if err := tmpl.Execute(os.Stdout, ""); err != nil { panic(err) } }
以上程序结果输出为 <Hello,go-zero>
。
- 带 pipeline 的子模板动作:
{{template "foo" pipeline}}
引用一个名为foo
的子模板,在该模板中,模板渲染时,会将pipeline
值传递给子模板。
package main import ( "os" "text/template" ) func main() { // 定义包含子模板 foo 的带参模板 const text = `{{define "foo"}}<Hello,{{.}}>{{end}}{{template "foo" .}}` tmpl := template.Must(template.New("main").Parse(text)) // 执行模板 if err := tmpl.Execute(os.Stdout, "go-zero"); err != nil { panic(err) } }
- block 动作:
{{block "foo" pipeline}}<Hello,go-zero>{{end}}
该模板表示创建了一个名为foo
的模板,且执行模板foo
。相当于{{define "foo"}}<Hello,pipeline>{{end}}{{template "foo" .}}
。 - with 动作:
{{with pipeline}}<Hello,go-zero>{{end}}
如果pipeline
的值为非0
值,则会pipeline
的值赋值给{{.}}
,然后输出<Hello,go-zero>
,如:
package main import ( "os" "text/template" ) func main() { // 定义子模板 const text = `{{with .action}}<Hello,{{.}}>{{end}}` tmpl := template.Must(template.New("main").Parse(text)) // 执行模板 if err := tmpl.Execute(os.Stdout, map[string]any{ "action": "go-zero", }); err != nil { panic(err) } }
以上程序输出内容为 <Hello,go-zero>
。
- with-else 动作:
{{with pipeline}}<Hello,go-zero>{{else}}<Hi,go-zero>{{end}}
如果pipeline
的值为非0
值,则会pipeline
的值赋值给{{.}}
,然后输出<Hello,go-zero>
,否则输出<Hi,go-zero>
,如:
package main import ( "os" "text/template" ) func main() { // 定义子模板 const text = `{{with .action}}<Hello,{{.}}>{{else}}<Hi,go-zero>{{end}}` tmpl := template.Must(template.New("main").Parse(text)) // 执行模板 if err := tmpl.Execute(os.Stdout, map[string]any{ "action": "go-zero", }); err != nil { panic(err) } }
以上程序输出内容为 <Hi,go-zero>
。
参数(Arguments)
参数是一个简单的值,通常有如下分类:
- 常量
包含布尔,字符串,字符,数值型常量,如{{"go"}}
、{{123}}
、{{true}}
。 - 变量
是一个以美元符号$
加上符合golang
变量命名规则的字母数字字符串组成,如{{$foo}}
、{{$x.Field}}
、{{$key}}
、{{$idx}}
、{{$value}}
、{{$Student.ID}}
等。变量一般在range
动作中使用比较多,比如range
时获取切片的序号{{$idx}}
和item
数据{{$item}}
等。 - 数据字段的名称
数据字段的名称,必须是结构体,前加英文点号.
,如{{.Field}}
,当然,如果数据字段还是结构体,则支持链式调用,如{{.Student.ID}}
,需要注意的是,对于结构体的字段名必须是可导出的(exported variable
),私有字段是无法读取的。 - map 的键名称
Map
的键的名称,数据类型必须是map
数据结构,前面必须有句点.
,键也支持链式调用,如{{.key.childKey}}
,和数据字段名称不同是,map
的键没有可导出这一要求。 - 方法
参数也可以是一个方法,即在模板中进行方法调用,且方法-结构体之间可以进行链式调用,如:
package main import ( "fmt" "os" "text/template" ) type Person struct { Name string Age int BestFriend *Person } func (p *Person) String() string { return fmt.Sprintf("name: %s, age: %d", p.Name, p.Age) } func main() { // {{.Name}} 代表【数据字段的名称】参数 // {{.String}} 代表【方法】参数 // {{.BestFriend.Name}} 代表【数据字段的名称】参数,链式调用 // {{.BestFriend.String}} 代表【数据字段的名称】和【方法】的链式参数调用 const text = `Hi, My name is {{.Name}}, This is my basic info: {{.String}}, My best friend is {{.BestFriend.Name}}, His info is {{.BestFriend.String}}` tmpl := template.Must(template.New("main").Parse(text)) // 执行模板 if err := tmpl.Execute(os.Stdout, &Person{ Name: "TonyGo", Age: 18, BestFriend: &Person{ Name: "TonyJava", Age: 29, }, }); err != nil { panic(err) } }
函数
函数包含预定义函数和扩展函数,扩展函数我们将在后续篇幅进行介绍。预定义函数信息如下:
函数与描述
and
逻辑与操作,返回两个或多个参数的逻辑与结果。call
用于调用用户扩展的函数,call
后面的入参根据函数定义来传,但是扩展函数必须满足如下规则:
- 函数返回参数必须为一个或者2个,1个时返回的是函数运行结果,类型
must-new
- 当返回参数为2个时,第二个参数必须是
error
类型。
html
返回安全的html
。此函数在html/template
中不可用,只有少数例外。index
返回切片、数组或映射的索引或键对应的值。slice
从切片或数组中提取子切片。假设x
为一个切片,因此"slice x 1 2"
在Go
语言中表示为x[1:2]
,而"slice x"
表示x[:]
,"slice x 1"
表示x[1:]
,"slice x 1 2 3"
表示x[1:2:3]
。第一个参数必须是字符串、切片或数组。js
返回安全的js
值。len
返回参数的长度。not
逻辑非操作,返回参数的逻辑非结果。or
逻辑或操作,返回两个或多个参数的逻辑或结果,即"or x y"
表现为"if x then x else y"
。评估按从左到右的顺序进行,当结果确定时返回。print
fmt.Sprint
的别名。printf
fmt.Sprintf
的别名。println
fmt.Sprintln
的别名。urlquery
返回其参数的文本表示形式的转义值,适合嵌入URL
查询中。此函数在html/template
中不可用,只有少数例外。
比较函数
eq
返回arg1 == arg2
的布尔值真假。ne
返回arg1 != arg2
的布尔值真假。lt
返回arg1 < arg2
的布尔值真假。le
返回arg1 <= arg2
的布尔值真假。gt
返回arg1 > arg2
的布尔值真假。ge
返回arg1 >= arg2
的布尔值真假。
管道(Pipelines
)
管道是一连串可能的 "命令"。命令是一个简单的值(参数)或函数或方法调用,管道可以由字符 |
分割指令序列,组成一条指令链。在链式管道中,每条命令的结果都会作为下一条命令的最后一个参数传递。流水线中最后一条命令的输出就是流水线的值。
注意
命令的输出要么是一个值,要么是两个值,其中第二个值 error
类型。如果存在第二个值 error
,且其值为非 nil
,即有错误抛出,则执行终止管道执行,错误信息将返回给 Execute
的调用者。
如下是一个简单的管道示例:
package main import ( "os" "text/template" ) type Data struct { Items []string } func main() { // 定义模板 tmpl := "{{ .Items | len | printf \"Number of items: %d\" }}" // 创建模板对象 t := template.New("example") // 解析并执行模板 t, _ = t.Parse(tmpl) data := Data{Items: []string{"Apple", "Banana", "Orange"}} t.Execute(os.Stdout, data) }
在此示例中,.Items
切片首先通过管道传递给 len
函数获取其长度,然后将结果传递给 printf
函数格式化输出。
总结
在本文中,我们深入探讨了 Go
语言生态系统中的模板引擎,特别是标准库中的 text/template
包。这个包不仅可以生成各种文本输出,还可以应用于诸如代码生成、HTML
、XML
等格式的文本文件处理中。通过学习本文,我们理解了 text/template
的基本概念和语法特性,例如如何定义和执行模板,以及如何利用动态指令和管道操作符 |
实现复杂的文本处理逻辑。
通过模板中的动作 {{}}
和静态文本的结合,我们学习了模板的渲染规则和格式化技巧,这些技巧可以帮助我们在模板设计时更加灵活和高效地控制输出内容的结构和格式。另外,我们还介绍了一些常用的动作,如条件判断、循环遍历、子模板定义等,这些动作使得模板引擎在处理复杂逻辑和大量数据时表现出色。
总体而言,text/template
包作为 Go
语言的一部分,为开发人员提供了一个强大且可靠的工具,用于生成和处理各种文本输出,是提升开发效率和代码质量的重要利器。深入理解和熟练掌握 text/template
的使用,将有助于我们在实际项目中更加轻松地处理和生成复杂的文本内容。