写点什么

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

作者:王中阳Go
  • 2023-09-13
    北京
  • 本文字数:5940 字

    阅读完需:约 19 分钟

你知道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 的基本使用。


首先声明一段模板


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: v1kind: Podmetadata:  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: v1kind: Podmetadata: 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 方式生成复用代码和模板代码,有利于我们解放双手。

一起进步

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


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


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


微信号:wangzhongyang1993


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


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

发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
你知道Golang的模板怎么用吗?带你了解动态文本的生成!_王中阳Go_InfoQ写作社区