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的基本运用。

首先声明一段模板

var md = `Hello,{{ . }}`

解析模板并履行

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上。

咱们再来看一个比如


// {{ .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的界说:

{{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接口类型。

var md = `个人信息:
名字: {{ .Name }}
年纪: {{ .Age }}
喜好: {{ .Hobby -}}
{{ if gt .Age 18 }}
成年人
{{ .Age | print }}
{{ else }}
未成年人
{{ end }}
`
// 输出
//个人信息:
//名字: Jackson       
//年纪: 20            
//喜好: 唱,跳,rap,篮球
//成年人              
//20 

循环操控range

template同时也供给了循环操控的功用。咱们仍是先看一下doc

{range pipeline}} T1 {{end}} pipeline 的值有必要是数组、切片、映射或通道。
	假如管道的值长度为零,则不输出任何内容;
	不然,将点设置为数组的接连元素,
	切片或映射并履行 T1。假如值是映射并且键是具有界说次序的基本类型,则将按排序键次序拜访
{{range pipeline}} T1 {{else}} T0 {{end}} 
	pipeline 的值有必要是数组、切片、映射或通道。
	假如管道的值长度为零,则 . 不受影响并
	履行 T0;不然,将 . 设置为数组、切片或映射的接连元素,并履行 T1。
{{break}}
	最里边的 {{range pipeline}} 循环提前结束,中止当时迭代并绕过一切剩余迭代。
{{continue}}
	最里边的 {{range pipeline}} 循环的越过当时迭代

整合上面的IF/ELSE,咱们做一个归纳事例

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去修正效果域。

咱们来看一个事例

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中。咱们经过查阅源码能够查看预界说了以下函数:

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能够转为小写。

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中咱们运用的会比较频频

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

{{template "name"}}
	具有指定称号的模板以无数据履行。
{{template "name" pipeline}}
	具有指定称号的模板以pipeline成果履行。

经过define界说模板称号

{{ define "container" }}
	模板
{{ end }}

经过template运用模板

{{ template "container" }}

咱们在运用template.New传入的name,实践上便是界说了模板的称号

事例:咱们期望抽象出Pod的container,经过代码来传入数据生成container,避免重复的编写yaml。

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常用函数

func Must(t *Template, err error) *Template

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

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

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

func (t *Template) Templates() []*Template

回来当时t相关的模板的slice,包含t本身。

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

传入模板称号,履行指定的模板。

假如在履行模板或写入其输出时产生过错,履即将中止,但部分成果或许现已被写入输出写入器。模板能够安全地并行履行,但假如并行履行同享一个Writer,则输出或许交错。

func (t *Template) Delims(left, right string) *Template

修正模板中的分界符,能够将{{}}修正为<>

func (t *Template) Clone() (*Template, error)

clone回来模板的副本,包含一切相关模板。在clone的副本上增加模板是不会影响原始模板的。所以咱们能够将其用于公共模板,经过clone获取不同的副本。

7.写在最终

Golang的template提高代码重用性:模板引擎答应你创建可重用的模板片段。经过将重复的模板逻辑提取到单独的模板中,并在需求时进行调用,能够减少代码重复,提高代码的可维护性和可扩展性。有许多code-gen运用了template + cobra办法生成复用代码和模板代码,有利于咱们解放双手。