goctl 技术系列 - text/template 深入讲解

简介: goctl 技术系列 - text/template 深入讲解

Go 语言的生态系统中,模板引擎是一个强大且实用的工具。text/template 包是 Go 语言标准库中的一部分,用于生成文本输出,例如 goctl 代码生成、HTMLXML 或其他格式的文本文件。在这篇文章中,我们将深入探讨 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 来进行渲染内容输出,因此如果并行执行模板渲染,可能导致内容错乱。

知识点:

  1. 模板变量接受任何格式的 UTF-8 编码文本
  2. 静态文本将原封不动复制输出

文本和空格(Text and spaces)

默认情况下,执行模板时,动作 (Action) 之间的所有文本都会被逐字复制,但是,为了帮助格式化模板源代码,模板渲染会按照如下规则进行格式化:

  • 规则1:如果一个操作的左分隔符(默认为 {{)后面紧跟着一个负号和空白,即 {{-   则 {{ 前一个文本中的所有尾部空白都会被删除。
  • 规则2:如果右分隔符 }} 前面有空白和减号,即 -}},则 }} 后面紧挨着的文本中所有前导空白都会被裁剪掉。
  • 规则3:如果需要做代码模板格式化,空白符号必须存在:{{- 3}}{{3 -}} 类似但会裁剪紧挨着的文本空白符号,而 {{-3}} 则解析为包含数字 -3 的操作。

如下是一段模板渲染的程序,变量 demo 接收 action1action2 两个参数,我们以如下程序来分别测试一下带有符号和空白符的模板渲染结果。

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 变量固定值为 Helloaction2 固定值为 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 来进行渲染内容输出,因此如果并行执行模板渲染,可能导致内容错乱。

知识点:

  1. 模板变量接受任何格式的 UTF-8 编码文本。
  2. 静态文本将原封不动复制输出。

文本和空格(Text and spaces)

默认情况下,执行模板时,动作 (Action) 之间的所有文本都会被逐字复制,但是,为了帮助格式化模板源代码,模板渲染会按照如下规则进行格式化:

  • 规则1:如果一个操作的左分隔符(默认为 {{)后面紧跟着一个负号和空白,即 {{-,则 {{ 前一个文本中的所有尾部空白都会被删除。
  • 规则2:如果右分隔符 }} 前面有空白和减号,即 -}},则 }} 后面紧挨着的文本中所有前导空白都会被裁剪掉。
  • 规则3:如果需要做代码模板格式化,空白符号必须存在:{{- 3}}{{3 -}} 类似但会裁剪紧挨着的文本空白符号,而 {{-3}} 则解析为包含数字 -3 的操作。

如下是一段模板渲染的程序,变量 demo 接收 action1action2 两个参数,我们以如下程序来分别测试一下带有符号和空白符的模板渲染结果。

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 变量固定值为 Helloaction2 固定值为 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\f0x850xA0

动作(Actions)

动作是模板中用于生成动态内容的指令,由 {{}} 包裹,常见动作有:

  • 注释动作{{/* a comment */}}
    注释动作会自动忽略多行注释,包含注释中的空白和换行内容。
  • pipeline 动作{{pipeline}}
    管道的默认动作是将文本值拷贝到输出,类似fmt.Print 做结果输出,当结合条件判断时,其有另外的意义。

例如:在上文代码中,模板 <{{.action1}} - {{.action2}}!> 中包含2个 pipeline 动作 {{.action1}}{{.action2}},其没有配合条件判断,因此其在模板渲染时直接当作文本值 HelloWorld 拷贝了标准输出,但当我们将模板换成 {{if .action1}} - {{.action2}}!{{end}}则结果输出为 - World!,这里的 pipeline .action1 就是一个条件表达式了,下文我们会对 pipeline 做详细说明。

注意

在动作中的 pipeline 并非管道的意思,可以通俗的理解为一个模板参数,与下文的管道并非同一个含义。

  • if 条件动作{{if pipeline}}<Hello World>{{end}}
    如果 pipeline 的值是非 0 值,则模板渲染后输出 <Hello World>,否则没有任何输出,{{if pipeline}}{{end}} 必须对应。

知识点:

0 值是指在没有明确初始化的情况下,变量的默认值。

  • 整数类型(如 intint32uint 等):零值为 0
  • 浮点类型(如 float32float64):零值为 0.0
  • 布尔类型:零值为 false
  • 字符串类型:零值为 ""(空字符串)
  • 指针类型:零值为 nil
  • 切片类型:零值为 length0
  • 映射类型:零值为 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

有意思的是,如果 rangepipelinemap 结构,且 mapkey 为基础数据类型,则 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 包。这个包不仅可以生成各种文本输出,还可以应用于诸如代码生成、HTMLXML 等格式的文本文件处理中。通过学习本文,我们理解了 text/template 的基本概念和语法特性,例如如何定义和执行模板,以及如何利用动态指令和管道操作符 | 实现复杂的文本处理逻辑。

通过模板中的动作 {{}} 和静态文本的结合,我们学习了模板的渲染规则和格式化技巧,这些技巧可以帮助我们在模板设计时更加灵活和高效地控制输出内容的结构和格式。另外,我们还介绍了一些常用的动作,如条件判断、循环遍历、子模板定义等,这些动作使得模板引擎在处理复杂逻辑和大量数据时表现出色。

总体而言,text/template 包作为 Go 语言的一部分,为开发人员提供了一个强大且可靠的工具,用于生成和处理各种文本输出,是提升开发效率和代码质量的重要利器。深入理解和熟练掌握 text/template 的使用,将有助于我们在实际项目中更加轻松地处理和生成复杂的文本内容。

项目地址

https://github.com/zeromicro/go-zero

相关文章
|
7月前
|
小程序 前端开发
【微信小程序】-- 常用的基础内容组件介绍 -- text & rich-text & progress & icon(七)
【微信小程序】-- 常用的基础内容组件介绍 -- text & rich-text & progress & icon(七)
|
6月前
|
JavaScript 程序员 编译器
type script Never
type script Never
|
7月前
|
前端开发
css教程-li的list-style-type属性
通过设置 `list-style-type`属性,你可以根据需求为列表项设置不同的标志样式,从而改变列表的外观。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
77 4
|
7月前
|
编译器 测试技术 调度
C++ 中 template<class T>和template<typename T>的区别
C++ 中 template<class T>和template<typename T>的区别
238 0
|
JavaScript 前端开发 数据安全/隐私保护
HTML中input常用的type属性及使用场景
HTML中input常用的type属性及使用场景
214 0
HTML中<button />和<input type=“button“/>的区别
HTML中<button />和<input type=“button“/>的区别
130 0
|
存储 编译器 C++
【C++模板】——template
【C++模板】——template
|
C#
Template、ItemsPanel、ItemContainerStyle、ItemTemplate
原文:Template、ItemsPanel、ItemContainerStyle、ItemTemplate 先来看一张图(网上下的图,加了几个字) 实在是有够“乱”的,慢慢来理一下; 1、Template是指控件的样式 在WPF中所有继承自contentcontrol类的控件都含有此属性,(继承自FrameworkElementdl类的TextBlock等控件无)。
737 0
|
关系型数据库 MySQL 应用服务中间件