不知道你有没有写过 helm chart,市面上大部分教程讲到 helm chart 的语法基本都是硬讲,死记硬背那些语法。。其实,chart 的语法正是 go template 的语法,所以只要系统的学习一下 go template,再来看 helm chart 基本一下子就了然于心了。。所以这里就有了这篇文章对 go template 的做下小结。
Golang 提供了两个标准库用来处理模板 text/template
和 html/template
,这俩库就类似于 Java 的 thymeleaf、freemarker 以及 Python 的 Jinjia2,也就是模板引擎库。
如其名,我们通常使用 text/template
来处理裸文本文档,使用 html/template
来处理 html 文档。
本篇文章后续示例我都会使用
html/template
,它们的接口几乎没有差异,了解一个另一个也就会了。
入门示例
下面看一个小例子:
package main
import (
"bytes"
"fmt"
"html/template"
"log"
)
func main() {
tplStr := `<a href="zze.xyz">{{ . }}</a>`
// 从字符串加载模板
tpl, err := template.New("tpl").Parse(tplStr)
// 从文件加载模板
// tpl, err := template.ParseFiles("tpl.html")
if err != nil {
log.Fatalf("parse failed, err: %v\n", err)
}
buf := new(bytes.Buffer)
err = tpl.Execute(buf, "hello zze")
//err = tpl.Execute(os.Stdout, "hello zze")
if err != nil {
log.Fatalf("exec tpl failed, err: %v\n", err)
}
fmt.Println(buf.String())
/*
<a href="zze.xyz">hello zze</a>
*/
}
在上述示例中,通过将模板(这里使用的是字符串,对应 tplStr
变量值,也可以使用一个文件)应用于一个对象(即该对象作为模板的参数)来执行,以获得输出。模板执行时会将指针指向当前操作对象,并表示为 .
。
用作模板的输入文本必须是 utf-8 编码的文本。在模板内容中通过 {{
和 }}
来界定参数值,在其之外的所有文本都直接原样拷贝到输出中。
函数
预置函数
go template 预置了很多常用的函数供我们使用,如下:
and
:同 shell 的&&
语义,第一个参数为真时,返回第二个参数,否则返回第一个参数;or
:同 shell 的||
语义,第一个参数为真时,返回第一个参数,否则返回第二个参数;not
:同 shell 的!
语义,取反返回;len
:返回参数值的长度;index
:返回以第一个参数为数组或字典,以第二个参数为索引或键指向的值;print
: 同fmt.Sprint
;printf
:同fmt.Sprintf
;println
:同fmt.Sprintln
;html
:转义 HTML 文本;urlquery
: 转义 URL 中的标点符号;js
:转义 javascript 脚本中的标点符号;call
:调用对象方法,改方法必须以属性的方式预定义到结构体;eq
:第一个参数和第二个参数相等则返回true
,否则返回false
;ne
:第一个参数和第二个参数不相等则返回true
,否则返回false
;lt
:第一个参数小于第二个参数则返回true
,否则返回false
;le
:第一个参数小于等于第二个参数则返回true
,否则返回false
;gt
:第一个参数大于第二个参数则返回true
,否则返回false
;ge
:第一个参数大于等于第二个参数则返回true
,否则返回false
;
看如下示例:
func main() {
tplStr := `
and: {{ and 0 1 }} | {{ and 2 1 }}
or: {{ or 0 1 }} | {{ or 2 1 }}
not: {{ not 0 }} | {{ not 1 }} | {{ not false }} | {{ not true }}
len: {{ len "zze" }}
index: {{ index .A 0 }} | {{ index .M 1 }}
print: {{ print .A }}
printf: {{ printf "hello %s" "zze" }}
println: {{ println "hello zze" }}
html: {{ html "<a href='zze.xyz'>zze</a>" }}
urlquery: {{ urlquery "www.zze.xyz?id=1" }}
js: {{ js "alert('123')" }}
call: {{ call .U.Show "test" }}
eq: {{ eq .U.Age 24 }}
ne: {{ ne .U.Age 24 }}
lt: {{ lt .U.Age 30 }}
le: {{ le .U.Age 30 }}
gt: {{ gt .U.Age 30 }}
ge: {{ ge .U.Age 30 }}
`
mySlice := []string{"a", "b", "c"}
myMap := make(map[int]string)
myMap[0] = "zze1"
myMap[1] = "zze2"
myUser := User{Id: 1, Name: "zze", Age: 24}
myUser.Show = func(remark string) string {
return fmt.Sprintf("my name is %s, %d years old. [%s]", myUser.Name, myUser.Age, remark)
}
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, struct {
A []string
M map[int]string
U User
}{mySlice, myMap, myUser})
/*
and: 0 | 1
or: 1 | 2
not: true | false | true | false
len: 3
index: a | zze2
print: [a b c]
printf: hello zze
println: hello zze
html: <a href='zze.xyz'>zze</a>
urlquery: www.zze.xyz%3Fid%3D1
js: alert(\'123\')
call: my name is zze, 24 years old. [test]
eq: true | true
ne: false
lt: true
le: true
gt: false
ge: false
*/
}
自定义函数
除了上述介绍的内置函数,go template 还支持自定义函数,看如下示例:
tplStr := `
{{ show "zze" }}
`
show := func(name string) string {
return fmt.Sprintf("%s 真帅!", "zze")
}
tpl, _ := template.New("tpl").Funcs(template.FuncMap{"show": show}).Parse(tplStr)
tpl.Execute(os.Stdout, "")
/*
zze 真帅!
*/
要注意的是自定义的函数必须在执行 Parse
方法前注册哦~
常用语法
渲染结构体
可以直接通过 .
来访问结构体对象中的属性。
tplStr := `
<ul>
<li>编号:{{ .Id }}</li>
<li>姓名:{{ .Name }}</li>
<li>年龄:{{ .Age }}</li>
</ul>
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, struct {
Id int
Name string
Age int
}{1, "zze", 24})
/*
<ul>
<li>编号:1</li>
<li>姓名:zze</li>
<li>年龄:24</li>
</ul>
*/
修改当前对象
可以使用 {{ with <target> }} ... {{ end }}
来将被 with
包裹的上下文的 .
的值设置为 target
。
tplStr := `
.Age: {{ .Age }}
{{ with .Age -}}
with <target>: {{ . -}}
{{ end }}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, struct {
Id int
Name string
Age int
}{1, "zze", 24})
/*
.Age: 24
with <target>: 24
*/
注释
被 {{/* ... */}}
包裹的内容不会被渲染。
tplStr := "hello {{/* a comment */}}"
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, "xxx")
/*
hello
*/
变量
模板中支持变量的定义和变量的赋值,同 go 语法,:=
代表初始化变量并赋值,=
仅代表赋值。
tplStr := `
{{- /* 声明变量并赋值 */ -}}
{{- $strLen := (len .) }}
长度:{{ $strLen }}
{{/* 赋值 */}}
{{- $strLen = 8 -}}
长度new:{{- $strLen }}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, "hello zze")
/*
长度:9
长度new:8
*/
条件判断
有如下三种标准的条件判断:
- 单分支:
{{ if <expr> }} t1... {{ end }}
; - 双分支:
{{ if <expr> }} t1... {{ else }} t2... {{end}}
; - 多分支:
{{ if <expr1> }} t1... {{ else if <expr2> }} t2... {{ else }} t3... {{ end }}
;
tplStr := `
{{ if lt .Age 18 }}
少年
{{ else if and (ge .Age 18) (lt .Age 35) }}
青年
{{ else if and (ge .Age 35) (lt .Age 50) }}
中年
{{ else }}
老年
{{ end }}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, struct {
Id int
Name string
Age int
}{1, "zze", 24})
/*
青年
*/
遍历
go template 也提供了类似 for
循环的遍历操作语法。
type User struct {
Id int
Name string
Age int
}
func main() {
tplStr := `
<ul>
{{- range . -}}
{{/* "." 遍历此段内容 */}}
<li>{{ .Id }}|{{ .Name }}|{{ .Age }}</li>
{{- else }}
{{/* "." 没有内容时才会执行此处 */}}
null
{{ end }}
</ul>
{{/* 获取遍历索引 */}}
<ul>
{{- range $i, $v := . }}
<li>{{ $i }} : {{ $v.Id }}|{{ $v.Name }}|{{ $v.Age}}</li>
{{- end }}
</ul>
`
users := make([]User, 0)
users = append(users, User{1, "zze", 94})
users = append(users, User{2, "zzf", 64})
users = append(users, User{3, "zzg", 24})
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, users)
/*
<ul>
<li>1|zze|94</li>
<li>2|zzf|64</li>
<li>3|zzg|24</li>
</ul>
<ul>
<li>0 : 1|zze|94</li>
<li>1 : 2|zzf|64</li>
<li>2 : 3|zzg|24</li>
</ul>
*/
}
去除空白
可以在 {{
符号的后面加上短横线并保留一个或多个空格 -
来去除它前面的空白(包括换行符、制表符、空格等),即 {{- xxxx
。
在 }}
的前面加上一个或多个空格以及一个短横线 -
来去除它后面的空白,即 xxxx -}}
。
tplStr := `
a {{ 111 }} b
a {{- 111 }} b
a {{ 111 -}} b
`
show := func(name string) string {
return fmt.Sprintf("%s 真帅!", "zze")
}
tpl, _ := template.New("tpl").Funcs(template.FuncMap{"show": show}).Parse(tplStr)
tpl.Execute(os.Stdout, "")
/*
a 111 b
a111 b
a 111b
*/
管道
前面介绍函数时所有函数的执行格式都是 <函数名> <参数1> <参数2>..
这种形式,其实它们还可以通过管道的方式来接收参数,看如下示例:
tplStr := `
{{ 18 | ge .Age }}
{{ .Name | show }}
`
show := func(name string) string {
return fmt.Sprintf("%s 真帅!", "zze")
}
tpl, _ := template.New("tpl").Funcs(template.FuncMap{"show": show}).Parse(tplStr)
tpl.Execute(os.Stdout, struct {
Id int
Name string
Age int
}{1, "zze", 24})
明确不转义
go template 默认情况下对于要传入模板渲染的入参都会进行转义以防止 XFS 攻击,比如:
tplStr := `
{{ . }}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, "<script>alert('asas')</script>")
/*
<script>alert('asas')</script>
*/
如果很明确不需要这个转义,可以使用 template.HTML
方法包裹传入的字符串:
tplStr := `
{{ . }}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, template.HTML("<script>alert('asas')</script>"))
/*
<script>alert('asas')</script>
*/
嵌套模板
可以通过 define <模板名>
在模板文件中预定义一些子模板块供后续 template <模板名>
调用。
tplStr := `
{{- define "T1" }} ONE {{ println . }}{{ end }}
{{- define "T2" }} TWO {{ println . }}{{ end }}
{{- define "T3" }}{{ template "T1" }}{{ template "T2" "haha" }}{{ end }}
{{- template "T3" -}}
`
tpl, _ := template.New("tpl").Parse(tplStr)
tpl.Execute(os.Stdout, "")
/*
ONE <nil>
TWO haha
*/
执行 template
时,第一个参数为 define
定义的模板名,第二个参数将会作为对应模板范围内的当前对象赋值给 .
。如上,调用 T1
时没有第二个参数,所以渲染结果为 ONE nil
,调用 T2
时第二个参数设置为了 haha
,所以最终渲染结果为 TWO haha
。
在 define
块内也可以通过 template
来调用其它已 define
的模板,如 T3
中调用了 T1
和 T2
。
template
不只能调用 define
块,还可以直接调用模板。看如下示例:
在 main.go
同级目录有如下两个文件:
1.html
<h1> in 1.html </h1>
{{ template "2.html" }}
2.html
<h1> in 2.html </h1>
main.go
内容如下:
func main() {
tpls, _ := template.ParseFiles("1.html", "2.html")
splitLine := strings.Repeat("-", 40)
// 遍历每个模板执行
for _, tpl := range tpls.Templates() {
fmt.Printf("%s %s %s\n", splitLine, tpl.Name(), splitLine)
tpl.Execute(os.Stdout, "")
fmt.Println()
}
fmt.Printf("%s 默认:%s %s\n", splitLine, tpls.Name(), splitLine)
// 直接调用 tpls 的 Execute 默认会渲染第一个模板,这里也就是 1.html
tpls.Execute(os.Stdout, "")
}
执行结果如下:
$ go run main.go
---------------------------------------- 1.html ----------------------------------------
<h1> in 1.html </h1>
<h1> in 2.html </h1>
---------------------------------------- 2.html ----------------------------------------
<h1> in 2.html </h1>
---------------------------------------- 默认:1.html ----------------------------------------
<h1> in 1.html </h1>
<h1> in 2.html </h1>
block 块
block 块可以在模板中预留一个位置并给这个位置设置一个默认值,看下面示例。
在 main.go
同级目录下有如下文件:
base.html
<title>{{block "title" .}}Default Title{{end}}</title>
<body>{{block "content" .}}This is the default body.{{end}}</body>
home.html
{{define "title"}}Home{{end}}
{{define "content"}}This is the Home page.{{end}}
about.html
{{define "title"}}About{{end}}
{{define "content"}}This is the About page.{{end}}
在 main.go
中来渲染 base.html
:
tpl, _ := template.ParseFiles("base.html")
tpl.Execute(os.Stdout, "")
/*
<title>Default Title</title>
<body>This is the default body.</body>
*/
由于仅解析了 base.html
,block
并不能发现到 home.html
或 about.html
中通过 define
定义的块的存在,所以此时就会输出 base.html
中 block
块中定义的默认内容。
修改 main.go
同时解析 base.html
和 home.html
:
tpl, _ := template.ParseFiles("base.html", "home.html")
tpl.Execute(os.Stdout, "")
/*
<title>Home</title>
<body>This is the Home page.</body>
*/
可以看到由于同时解析了 home.html
和 base.html
,所以 base.html
中的 block
块能够发现 home.html
中通过 define
定义的 title
和 content
块,就会使用它们替换对应的 block
块。
同理,同时解析 base.html
和 about.html
效果如下:
tpl, _ := template.ParseFiles("base.html", "about.html")
tpl.Execute(os.Stdout, "")
/*
<title>About</title>
<body>This is the About page.</body>
*/
评论区