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

一起进步

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

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

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

微信号:wangzhongyang1993

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

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

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

  1. 关于模板该不该用css强制编辑器文本开头空两格

    关于模板该不该用css强制编辑器文本开头空两格这个问题,我很早之前就想说了,写惯了qq日志的童鞋都知道,qq空间的编辑器没有任何css控制,行头空两格是由用户自己控制,我写起日志又像流水账,长长的一篇 ...

  2. AutoSharedLibrary -- 基于模板元编程技术的跨平台C++动态链接载入库

    基于模板元编程技术的跨平台C++动态链接载入库.通过模板技术,使用者仅需通过简单的宏,就可以使编译器在编译期自己主动生成载入动态链接库导出符号的代码,无不论什么额外的执行时开销. extern &qu ...

  3. 使用不同模板引擎beetl、FreeMarker、Velocity动态解析sql的方法

    1. String sql = null;if(null == renderType || renderType.equals(ConstantRender.sql_renderType_beetl) ...

  4. ThinkPHP模板中JS等带花括号处会被解析错误的解决办法

    如下图,当本人在ThinkPHP框架的模板中写jQuery代码的时候,写了一些注释,并且注重是斜线和换括号{是连着一起的,这层语法上来时是没问题的,但是在ThinkPHP 的模板引擎解析下,会被解析掉 ...

  5. js模板(template.js)实现页面动态渲染

    template.js是由纯JavaScript编写的第三方模板引擎.点击https://github.com/yanhaijing/template.js可进行下载. 在页头导入模板文件 <s ...

  6. goweb-模板引擎

    模板引擎 Go 为我们提供了 text/template 库和 html/template 库这两个模板引擎,模板引 擎通过将数据和模板组合在一起生成最终的 HTML,而处理器负责调用模板引擎并将引 ...

  7. golang 模板(template)的常用基本语法

    1. 模板 在写动态页面的网站的时候,我们常常将不变的部分提出成为模板,可变部分通过后端程序的渲染来生成动态网页,golang提供了html/template包来支持模板渲染. 这篇文章不讨论gola ...

  8. golang路上的小学生系列--使用reflect查找package路径

    本文同时发布在个人博客chinazt.cc 和 gitbook 今日看到了一个有趣的golang项目--kolpa(https://github.com/malisit/kolpa). 这个项目可以用 ...

  9. golang 特殊知识点

    golang 代码不需要分号; 但是又会自己在底层增加;号 ,所以 golang的{左花括号必须在代码的最后一行,而不能在新的一行; golang 代码组织里需要注意 vendor 和 interna ...

  10. golang iris html/temple

    在使用golang的模板语法的过程中遇见自动转义问题(或者以我的理解下发的富文本html代码不是template.html类型,而是string类型),需要强制转型 func unescaped(x ...

随机推荐

  1. rest --framework 源码学习第一天 view 类

    首先view类:源码分析

  2. Vue——表单控制、购物车案例、v-model进阶、与后端交互三种方式、箭头函数

    表单控制 // 1 checkbox 单选 多选 // 2 radio 单选 <body> <div id="app"> <h1>checkbo ...

  3. Dapr在Java中的实践 之 服务调用

    服务调用 通过服务调用(Service-to-service Invocation),服务可以使用 gRPC 或 HTTP 这样的标准协议来发现并可靠地与其他服务通信. Dapr采用边车(Sideca ...

  4. 浅谈 ByteHouse Projection 优化实践

    预聚合是 OLAP 系统中常用的一种优化手段,在通过在加载数据时就进行部分聚合计算,生成聚合后的中间表或视图,从而在查询时直接使用这些预先计算好的聚合结果,提高查询性能,实现这种预聚合方法大多都使用物 ...

  5. Python-PyQt5的安装与简单使用

    一.安装 1.安装 PyQt5 和 PyQt5-tools pip install PyQt5 -i https://pypi.douban.com/simple pip install PyQt5- ...

  6. NFS远程挂载

    NFS远程挂载 一.概述 NFS是一种基于TCP/IP 传输的网络文件系统协议.通过使用NFS协议,客户机可以像访问本地目录一样访问远程服务器中的共享资源 NAS存储: NFS服务的实现依赖于RPC ...

  7. go语言字符与字符串相关

    ASCII ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁 字母的一套单字节编码系统 字符 本质上来 ...

  8. 浅聊一下 C#程序的 内存映射文件 玩法

    一:背景 1. 讲故事 前段时间训练营里有朋友问 内存映射文件 是怎么玩的?说实话这东西理论我相信很多朋友都知道,就是将文件映射到进程的虚拟地址,说起来很容易,那如何让大家眼见为实呢?可能会难倒很多人 ...

  9. 反转链表 Java版 图文并茂思路分析带答案(力扣第206题)

    反转链表 力扣第206题 我们不只是简单的学习(背诵)一个数据结构,而是要分析他的思路,以及为什么要有不同的指针等等 非递归方式: 思路分析:首先要链表有个头指针没有任何问题 然后,我们要将1的下一个 ...

  10. 基于JavaFX的扫雷游戏实现(二)——游戏界面

      废话环节:看过上期文章的小伙伴现在可能还是一头雾水,怎么就完成了核心内容,界面呢?哎我说别急让我先急,博主这不夜以继日地肝出了界面部分嘛.还是老规矩,不会把所有地方都照顾到,只挑一些有代表性的内容 ...