解锁Golang模板的力量:动态文本生成的初学者指南

2023年 8月 30日 50.9k 0

Golang Template

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

Go Template具有以下主要特性:

  • 简洁易用:Go Template语法简洁而易于理解。它使用一对双大括号“{{}}”来标记模板的占位符和控制结构。这种简单的语法使得模板的编写和维护变得非常方便。
  • 数据驱动:Go Template支持数据驱动的模板生成。你可以将数据结构传递给模板,并在模板中使用点号“.”来引用数据的字段和方法。这种数据驱动的方式使得模板可以根据不同的数据动态生成输出。
  • 条件和循环:Go Template提供了条件语句和循环语句,使得你可以根据条件和迭代来控制模板的输出。你可以使用“if”、“else”、“range”等关键字来实现条件判断和循环迭代,从而生成灵活的输出。
  • 过滤器和函数:Go Template支持过滤器和函数,用于对数据进行转换和处理。你可以使用内置的过滤器来格式化数据,例如日期格式化、字符串截断等。此外,你还可以定义自己的函数,并在模板中调用这些函数来实现更复杂的逻辑和操作。
  • 嵌套模板: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, //  {{ $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方式生成复用代码和模板代码,有利于我们解放双手。

    原文链接:https://juejin.cn/post/7255189463746953271

    本文转载自微信公众号「 程序员升级打怪之旅」,作者「 Shyunn&王中阳Go」,可以通过以下二维码关注。

    转载本文请联系「 程序员升级打怪之旅」公众号。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论