你知道Golang的模板怎么用吗?带你了解动态文本的生成!

Golang Template

Go语言中的Go Template是一种用于生成文本输出的简单而强大的模板引擎。它提供了一种灵活的方式来生成各种格式的文本,例如HTML、XML、JSON等。

Go Template的具有以下主要特性:

  1. 简洁易用:Go Template语法简洁而易于理解。它使用一对双大括号"{{}}"来标记模板的占位符和控制结构。这种简单的语法使得模板的编写和维护变得非常方便。
  2. 数据驱动:Go Template支持数据驱动的模板生成。你可以将数据结构传递给模板,并在模板中使用点号"."来引用数据的字段和方法。这种数据驱动的方式使得模板可以根据不同的数据动态生成输出。
  3. 条件和循环:Go Template提供了条件语句和循环语句,使得你可以根据条件和迭代来控制模板的输出。你可以使用"if"、"else"、"range"等关键字来实现条件判断和循环迭代,从而生成灵活的输出。
  4. 过滤器和函数:Go Template支持过滤器和函数,用于对数据进行转换和处理。你可以使用内置的过滤器来格式化数据,例如日期格式化、字符串截断等。此外,你还可以定义自己的函数,并在模板中调用这些函数来实现更复杂的逻辑和操作。
  5. 嵌套模板:Go Template支持模板的嵌套,允许你在一个模板中包含其他模板。这种模板的组合和嵌套机制可以帮助你构建更大型、更复杂的模板结构,提高代码的可重用性和可维护性。

在很多Go开发的工具,项目都大量的使用了template模板。例如: Helm,K8s,Prometheus,以及一些code-gen代码生成器等等。Go template提供了一种模板机制,通过预声明模板,传入自定义数据来灵活的定制各种文本。

1.示例

我们通过一个示例来了解一下template的基本使用。

首先声明一段模板

go 复制代码
var md = `Hello,{{ . }}`

解析模板并执行

scss 复制代码
func main() {
	tpl := template.Must(template.New("first").Parse(md))
	if err := tpl.Execute(os.Stdout, "Jack"); err != nil {
		log.Fatal(err)
	}
}

// 输出
// Hello Jack

在上述例子中, {{ . }}前后花括号属于分界符,template会对分界符内的数据进行解析填充。其中 .代表当前对象,这种概念在很多语言中都存在。

在main函数中,我们通过template.New创建一个名为"first"的template,并用此template进行Parse解析模板。随后,再进行执行:传入io.Writer,data,template会将数据填充至解析的模板中,再输出到传入的io.Writer上。

我们再来看一个例子

go 复制代码
// {{ .xxoo -}} 删除右侧的空白
var md = `个人信息:
姓名: {{ .Name }}
年龄: {{ .Age }}
爱好: {{ .Hobby -}}
`

type People struct {
	Name string
	Age  int
}

func (p People) Hobby() string {
	return "唱,跳,rap,篮球"
}

func main() {

	tpl := template.Must(template.New("first").Parse(md))
	p := People{
		Name: "Jackson",
		Age:  20,
	}
	if err := tpl.Execute(os.Stdout, p); err != nil {
		log.Fatal(err)
	}
}

// 输出
//个人信息:
//姓名: Jackson       
//年龄: 20            
//爱好: 唱,跳,rap,篮球

Hobby属于People的方法,所以在模板中也可以通过.进行调用。需要注意: 不管是字段还是方法,由于template实际解析的包与当前包不同,无论是字段还是方法必须是导出的。

在template中解析时,它 移除了 {{}} 里面的内容,但是留下的空白完全保持原样。所以解析出来的时候,我们需要对空白进行控制。YAML认为空白是有意义的,因此管理空白变得很重要。我们可以通过-进行控制空白。

{{- (包括添加的横杠和空格)表示向左删除空白, 而 -}}表示右边的空格应该被去掉。

要确保-和其他命令之间有一个空格。

{{- 10 }}: "表示向左删除空格,打印10"

{{ -10 }}: "表示打印-10"

2.流程控制

条件判断 IF ELSE

在template中,提供了if/else的流程判断。

我们看一下doc的定义:

lua 复制代码
{{if pipeline}} T1 {{end}}
	如果 pipeline 的值为空,则不生成输出;
	否则,执行T1。空值为 false、0、任何
	nil 指针或接口值,以及
	长度为零的任何数组、切片、映射或字符串。
	点不受影响。
{{if pipeline}} T1 {{else}} T0 {{end}}
	如果 pipeline 的值为空,则执行 T0;
	否则,执行T1。点不受影响。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
	为了简化 if-else 链的外观,
	if 的 else 操作可以直接包含另一个 if

其中pipeline 命令是一个简单的值(参数)或一个函数或方法调用。我们第一个例子的hobby就属于方法调用。

继续是上面的案例,我们添加了一个IF/ELSE来判断年龄,在IF中我们使用了一个内置函数gt判断年龄。

在template中,调用函数,传递参数是跟在函数后面: function arg1 agr2

或者也可以通过管道符进行传递:arg | function

每个函数都必须有1到2个返回值,如果有2个则后一个必须是error接口类型。

arduino 复制代码
var md = `个人信息:
姓名: {{ .Name }}
年龄: {{ .Age }}
爱好: {{ .Hobby -}}
{{ if gt .Age 18 }}
成年人
{{ .Age | print }}
{{ else }}
未成年人
{{ end }}
`

// 输出
//个人信息:
//姓名: Jackson       
//年龄: 20            
//爱好: 唱,跳,rap,篮球
//成年人              
//20 

循环控制range

template同时也提供了循环控制的功能。我们还是先看一下doc

go 复制代码
{range pipeline}} T1 {{end}} pipeline 的值必须是数组、切片、映射或通道。
	如果管道的值长度为零,则不输出任何内容;
	否则,将点设置为数组的连续元素,
	切片或映射并执行 T1。如果值是映射并且键是具有定义顺序的基本类型,则将按排序键顺序访问
	
{{range pipeline}} T1 {{else}} T0 {{end}} 
	pipeline 的值必须是数组、切片、映射或通道。
	如果管道的值长度为零,则 . 不受影响并
	执行 T0;否则,将 . 设置为数组、切片或映射的连续元素,并执行 T1。
	
{{break}}
	最里面的 {{range pipeline}} 循环提前结束,停止当前迭代并绕过所有剩余迭代。
	
{{continue}}
	最里面的 {{range pipeline}} 循环的跳过当前迭代

整合上面的IF/ELSE,我们做一个综合案例

go 复制代码
var md = `
Start iteration:
{{- range . }}
{{- if gt . 3 }}
超过3
{{- else }}
{{ . }}
{{- end }}
{{ end }}
`

func main() {
	tpl := template.Must(template.New("first").Parse(md))
	p := []int{1, 2, 3, 4, 5, 6}
	if err := tpl.Execute(os.Stdout, p); err != nil {
		log.Fatal(err)
	}
}

// 输出
//1       
//2        
//3       
//超过3    
//超过3    
//超过3

我们通过{{ range . }}遍历传入的对象,在循环内部再通过{{ if }}/{{ else }}判断每个元素的大小。

作用域控制with

在语言中都有一个作用域的概念。template也提供了通过使用with去修改作用域。

我们来看一个案例

csharp 复制代码
var md = `
people name(out scope): {{ .Name }}
dog name(out scope): {{ .MyDog.Name }}
{{- with .MyDog }}
dog name(in scope): {{ .Name }} 
people name(in scope): {{ $.Name }}
{{ end }}
`
type People struct {
	Name  string
	Age   int
	MyDog Dog
}

type Dog struct {
	Name string
}

func main() {
	tpl := template.Must(template.New("first").Parse(md))
	p := People{Name: "Lucy", MyDog: Dog{Name: "Tom"}}
	if err := tpl.Execute(os.Stdout, p); err != nil {
		log.Fatal(err)
	}
}

// 输出
//people name(out scope): Lucy
//dog name(out scope): Tom    
//dog name(in scope): Tom     
//people name(in scope): Lucy 

在顶层作用域中,我们直接可以通过.去获取对象的信息。在声明的with中,我们将顶层对象的MyDog传入,那么在with作用域中,通过.获取的对象就是Dog。所以在with中我们可以直接通过.获取Dog的name。

有些时候,在子作用域中我们可能也希望可以获取到顶层对象,那么我们可以通过$获取顶层对象。上述例子的$.获取到People。

3.函数

在第二节内容中,我们使用了print,gt函数,这些函数都是预定义在template中。我们通过查阅源码可以查看预定义了以下函数:

perl 复制代码
func builtins() FuncMap {
	return FuncMap{
		"and":      and,
		"call":     call,
		"html":     HTMLEscaper,
		"index":    index,
		"slice":    slice,
		"js":       JSEscaper,
		"len":      length,
		"not":      not,
		"or":       or,
		"print":    fmt.Sprint,
		"printf":   fmt.Sprintf,
		"println":  fmt.Sprintln,
		"urlquery": URLQueryEscaper,

		// Comparisons
		"eq": eq, // ==
		"ge": ge, // >=
		"gt": gt, // >
		"le": le, // <=
		"lt": lt, // <
		"ne": ne, // !=
	}
}

在实际开发中,仅仅是这些函数是很难满足我们的需求。此时,我们希望能够传入自定义函数,在我们编写模板的时候可以使用自定义的函数。

我们引入一个需求: 希望将传入的str可以转为小写。

go 复制代码
var md = `
result: {{ . | lower }}
`

func Lower(str string) string {
	return strings.ToLower(str)
}

func main() {
	tpl := template.Must(template.New("demo").Funcs(map[string]any{
		"lower": Lower,
	}).Parse(md))
	tpl.Execute(os.Stdout, "HELLO FOSHAN")
}

// 输出
// result: hello foshan

由于template支持链式调用,所以我们一般把Parse放在最后

我们通过调用Funcs,传入functionName : function的map。

执行模板时,函数从两个函数map中查找:首先是模板函数map,然后是全局函数map。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

方法必须有一到两个返回值,如果是两个,那么第二个一定是error接口类型

注意:Funcs必须在解析parse前调用。如果模板已经解析了,再传入funcs,template并不知道该函数应该如何映射。

4.变量

函数、管道符、对象和控制结构都可以控制,我们转向很多编程语言中更基本的思想之一:变量。 在模板中,很少被使用。但是我们可以使用变量简化代码,并更好地使用withrange

我们通过{{ $var := .Obj }}声明变量,在with/range中我们使用的会比较频繁

php 复制代码
var md = `
{{- $count := len . -}}
共有{{ $count }}个元素
{{- range $k,$v := . }}
{{ $k }} => {{ $v }}
{{- end }}
`

func main() {
	tpl := template.Must(template.New("demo").Parse(md))
	tpl.Execute(os.Stdout, map[string]string{
		"p1": "Jack",
		"p2": "Tom",
		"p3": "Lucy",
	})
}

// 输出
// 共有3个元素
// p1 => Jack 
// p2 => Tom  
// p3 => Lucy 

{{ var }}声明的变量也有作用域的概念,如果在顶层作用域中声明了var,那么在内部作用域可以直接通过获取该变量

我们通过{{- range $k,$v := . }}遍历map中每一个KV,这种写法类似于Golang的for-range

5.命名模板

在Go语言的模板引擎中,命名模板是指通过给模板赋予一个唯一的名称,将其存储在模板集中,以便后续可以通过该名称来引用和执行该模板。

通过使用命名模板,你可以将一组相关的模板逻辑组织在一起,并在需要的时候方便地调用和重用它们。这对于构建复杂的模板结构和提高模板的可维护性非常有用。

在编写复杂模板的时候,我们总是希望可以抽象出公用模板,那么此时就需要使用命名模板进行复用。

本节将基于K8sPod模板的案例来学习如何使用命名模板进行抽象复用。

我们看一下doc

arduino 复制代码
{{template "name"}}
	具有指定名称的模板以无数据执行。

{{template "name" pipeline}}
	具有指定名称的模板以pipeline结果执行。

通过define定义模板名称

sql 复制代码
{{ define "container" }}
	模板
{{ end }}

通过template使用模板

arduino 复制代码
{{ template "container" }}

我们在使用template.New传入的name,实际上就是定义了模板的名称

案例:我们希望抽象出Pod的container,通过代码来传入数据生成container,避免重复的编写yaml。

vbnet 复制代码
var pod = `
apiVersion: v1
kind: Pod
metadata:
  name: "test"
spec:
  containers:
{{- template "container" .}}
`
var container = `
{{ define "container" }}
    - name: {{ .Name }}
      image: "{{ .Image}}"
{{ end }}
`

func main() {
	tpl := template.Must(template.New("demo").Parse(pod))
	tpl.Parse(container)
	tpl.ExecuteTemplate(os.Stdout, "demo", struct {
		Name  string
		Image string
	}{
		"nginx",
		"1.14.1",
	})
}

// 输出
apiVersion: v1
kind: Pod
metadata:
  name: "test"
spec:
  containers:
    - name: nginx    
      image: "1.14.1"

tpl可以解析多个模板,在不同模板中通过define定义模板即可。使用ExecuteTemplate传入模板名指定解析模板。在{{- template "container" .}}中可以传入对象数据。

在实际开发中,我们往往不会采用打印的方式输出。可以根据不同的需求,在Execute执行时选择不同的io.Writer。往往我们更希望写入到文件中。

6.Template常用函数

go 复制代码
func Must(t *Template, err error) *Template

Must是一个helper函数,它封装对返回(Template, error)的函数的调用,并在错误非nil时panic。它旨在用于template初始化。

go 复制代码
// 解析指定文件
// 示例: ParseFiles(./pod.tpl) 
func ParseFiles(filenames ...string) (*Template, error)


// 解析filepath.Match匹配文件
// 示例: ParseGlob(/data/*.tpl)
func ParseGlob(pattern string) (*Template, error)

这两个函数帮助我们解析文件中的模板,大多数情况下我们都是将模板写在.tpl结尾的文件中。通过不同的解析规则解析对应的文件。

scss 复制代码
func (t *Template) Templates() []*Template 

返回当前t相关的模板的slice,包括t本身。

go 复制代码
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

传入模板名称,执行指定的模板。

如果在执行模板或写入其输出时发生错误,执行将停止,但部分结果可能已经被写入输出写入器。模板可以安全地并行执行,但如果并行执行共享一个Writer,则输出可能交错。

go 复制代码
func (t *Template) Delims(left, right string) *Template

修改模板中的分界符,可以将{{}}修改为<>

scss 复制代码
func (t *Template) Clone() (*Template, error) 

clone返回模板的副本,包括所有关联模板。在clone的副本上添加模板是不会影响原始模板的。所以我们可以将其用于公共模板,通过clone获取不同的副本。

7.总结

Golang的template提高代码重用性:模板引擎允许你创建可重用的模板片段。通过将重复的模板逻辑提取到单独的模板中,并在需要时进行调用,可以减少代码重复,提高代码的可维护性和可扩展性。有许多code-gen使用了template + cobra方式生成复用代码和模板代码,有利于我们解放双手。

一起进步

原文链接:mp.weixin.qq.com/s/SXQt6aPTj...

​独行难,众行易,一个人刻意练习是​孤独的。

欢迎加入我们的小圈子,一起刻意练习,结伴成长!

微信号:wangzhongyang1993

公众号:程序员升职加薪之旅

也欢迎大家关注我的账号 ,点赞、留言、转发。你的支持,是我更文的最大动力!

相关推荐
devlei2 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑4 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3564 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3564 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁5 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp5 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴6 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友6 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒7 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan8 小时前
Go 内存回收-GC 源码1-触发与阶段
后端