go template使用

  • 2022-10-31
  • 浏览 (1761)

以text/template为例, 而html/template的接口与前者一样,不再缀述。

模板文件一般由 .tmpl.tpl 为后缀。

一些名词

dot:用表示 .,相当于一个变量,保存着传进来的值,可以改变

pipeline:从字面上看,有点像管道 |,但从文档上看,实际上指的是一切取值操作,包括 {{ . }}{{ $name }},而 | 与unix中的一样:作为函数的最后一个参数

{{ }}:相当于占位符,主要的逻辑都写在里面

1 模板定义

1.1 取值

取值的作用主要是在页面中表示出来,或者使用一个变量保存

类型方式解释当前值{{ . }}传什么值,就取什么值,假如直接在页面上输出的话,类似fmt.Println结构体{{ .Field }}Field 指的是字段名,假如结构体嵌套,还可以再使用 . 取值,注意:遵循可见性规则变量{{ $varName }}$ 开头,取出变量的值,如何定义且看 1.2字典{{ .key }}取字典key对应的值,不需要首字母大写,嵌套时,可以再使用 . 取值无参数方法{{ .Method }}执行Method这个方法,第一个返回值作为取出的值,注意:遵循可见性规则,而且返回值有要求,详细见xxx无参数函数{{ func }}执行func(),把返回值当做结果,详见xxx

1.2 变量

有些值,我们可能需要重复使用,最好的方法就是使用一个变量来保存值减少重复求值的过程。

// 用到的数据
name := "abcdef"

假如我们把name传进来,那么假如要求其长度并将其保存起来,可以使用一个内置函数(见1.4): len

在go template中,用 $ 表示变量,有点类似shell,使用:

{{ $lenght := len . }}
<h1>长度:{{ $lenght }}</h1>

实际上,还可以这样写:

{{ $lenght := . | len }}
<h1>长度:{{ $lenght }}</h1>

利用 |abcdef 当做最后一个参数传给 len

1.3 动作

go template的动作(action)有点像,django的模板引擎中的tag,不过两者之间还是有较大的不一样。

1.3.1 注释

注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止,就像这里表示的一样。

{{/* 我是注释啊 */}}

1.3.2 if判断

有以下3种

  1. {{if pipeline}} T1 {{end}}

如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。

Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。

如:

<p>{{ if . }}welcome{{ end }}</p>

在这里我传的是一个bool值,为true,因此p便签中的内容为welcome

  1. {{if pipeline}} T1 {{else}} T0 {{end}}

如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。

<p>{{ if . }}welcome{{ else }}  Get out!{{ end }}</p>

  1. {{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

用于简化if-else链条,else action可以直接包含另一个if;等价于:

{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

<p>{{ if eq . 1 }}count=1{{ else if eq . 2}}  count=2{{ else if eq . 3}}  count=3{{ end }}</p>

这里的 eq 是一个内置函数,相当于 ==

1.3.3 with

这里的 with 与并不是Python的with。go template的 with 相当于可以暂时修改dot的if。

形式一{{with pipeline}} T1 {{end}}

如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{ with gt . 18}} result:{{ . }}, 嘿嘿嘿 {{end}}

这里的gt相当于 >, 因此假如执行成功,那么 . 必然是 true.

形式二{{with pipeline}} T1 {{else}} T0 {{end}}

如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。不修改外面的dot。

实际上这和上面的一样,就是多了个 {{ else }}

{{ with gt . 18}} result:{{ . }}, 嘿嘿嘿 {{else}} 才{{ . }}岁,未成年 {{end}}

1.3.4 遍历

遍历的值必须是数组、切片、字典或者通道。

  1. 简单形式: {{range pipeline}} T1 {{end}}

如果pipeline的值其长度为0,不会有任何输出;

否则dot依次设为数组、切片、字典或者通道的每一个成员元素并执行T1;

如果pipeline的值为字典,且键可排序的基本类型,元素也会按键的顺序排序。

如,要遍历的数据如下:

data := map[string]string{
		"张三": "hello",
		"李四": "word",
	}

在模板文件中定义:

<div>
    {{ range . }}
    <p>{{ . }}</p>
    {{ end }}
</div>

所得到的结果:

<div>
    <p>hello</p>
    <p>word</p>
</div>

  1. 加else形式: {{range pipeline}} T1 {{else}} T0 {{end}}

如果pipeline的值其长度为0,不改变dot的值并执行T0;否则会修改dot并执行T1。

假如数据是一个空切片 []int{}

<div>
    {{ range . }}
        <p>{{ . }}</p>
    {{ else }}
        <span>no data</span>
    {{ end }}
</div>

结果是 <span>no data</span>

1.3.5 嵌套与继承

define

当解析模板时,可以定义另一个模板,该模板会和当前解析的模板相关联。 模板必须定义在当前模板的最顶层,就像go程序里的全局变量一样。

这种定义模板的语法是将每一个子模板的声明放在”define”和”end” action内部。

如:

{{ define "rd"}}
    <div>
    {{ range . }}
        <p>{{ . }}</p>
    {{ else }}
        <span>v2 no data</span>
    {{ end }}
</div>
{{ end }}

注意:结尾 {{ end }}define 后面的是字符串

template

template 就是对 define 定义的模板或其他模板文件的引用。

template的形式

  • {{template "name"}}

执行名为name的模板,提供给模板的参数为nil,如模板不存在输出为”” - {{template "name" pipeline}}

执行名为name的模板,提供给模板的参数为pipeline的值。

如,在当前文件中引用:

{{ define "rd"}}
    <div>
    {{ range . }}
        <p>{{ . }}</p>
    {{ else }}
        <span>v2 no data</span>
    {{ end }}
</div>
{{ end }}

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>template</title>
</head>

<body>
{{/*引用*/}}
{{ template "rd"}}
</body>
</html>

template引用其他文件注意:

  1. 千万注意,要在代码中把文件读进来。

t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")

也可以使用其他函数 2. template后面跟的是完整的文件名

在h1.tpl中: {{ template "h2.tpl" }}

{{template "name" pipeline}} 形式

就是把define中或文件中的 . 替换成template传进去的值,假如不指定的话,使用当前文件的 .

{{ define "say"}}
    <h1>say {{ . }}</h1>
{{ end }}
{{ template "say" "hi"}}

结果: <h1>say hi</h1>

block

block是定义模板 {{define "name"}} T1 {{end}} 和执行 {{template "name" pipeline}} 缩写,典型的用法是定义一组根模板,然后通过在其中重新定义块模板进行自定义。

如,在 ./templates/base.tpl 中,定义:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
    {{block "content" . }}{{end}}
</div>
</body>
</html>

而在其他的模板文件中:

{{template "base.tpl"}}

{{/* 使用 */}}
{{define "content"}}
	<!-- 写入自己的代码 -->
    <div>Hello world!</div>
{{end}}

同样要注意,在解析文件时把多个模板文件传进来

1.3.6 去空

{{- . -}}

使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。

1.4 函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

1.4.1 一般函数

  • and

函数返回它的第一个empty参数或者最后一个参数;

就是说”and x y”等价于”if x then y else x”;所有参数都会执行;

和上面一样:Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。下面再重复

如:

{{ and 1 0 }}
{{/* 返回0 */}}

{{ and 1 1 1}}
{{/* 返回1 */}}

  • or

返回第一个非empty参数或者最后一个参数;

亦即”or x y”等价于”if x then x else y”;所有参数都会执行;

如:

{{ or 1 0 }}
{{/* 返回1 */}}

{{ or 0 2 1}}
{{/* 返回2 */}}

  • not

返回它的单个参数的布尔值的否定

如:

{{ not 1 }}
{{/* 返回false */}}

{{ not 0 }}
{{/* 返回true */}}

  • len

返回它的参数的整数类型长度

如:

{{/*  . 为"abcdef"  */}}
{{ len . }}

{{/*  返回6  */}}

  • index

执行结果为第一个参数以剩下的参数为索引/键指向的值;

如”index x 1 2 3”返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。

假如数据为:

	data := [][]int{
		{1, 2, 3, 4, 5,},
		{6, 7, 8, 9, 10,},
	}

{{ index . 0 1}}

{{/* 结果为2 */

  • print

即fmt.Sprint

S系列函数会把传入的数据生成并返回一个字符串。以下两个相同。

  • printf

即fmt.Sprintf

  • println

即fmt.Sprintln

  • html

返回与其参数的文本表示形式等效的转义HTML。

这个函数在 html/template不可用

  • urlquery

以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。

这个函数在 html/template不可用

  • js

返回与其参数的文本表示形式等效的转义JavaScript。

  • call

执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;

{{ call .X.Y 1 2 }} 等价于go语言里的 dot.X.Y(1, 2)

其中Y是函数类型的字段或者字典的值,或者其他类似情况;

call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);

该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;

如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

1.4.2 布尔函数

布尔函数会将任何类型的零值视为假,其余视为真。

函数说明eq如果arg1 == arg2则返回真ne如果arg1 != arg2则返回真lt如果arg1 < arg2则返回真le如果arg1 <= arg2则返回真gt如果arg1 > arg2则返回真ge如果arg1 >= arg2则返回真

注意:

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:

arg1 == arg2 || arg1 ==arg3 || arg1==arg4 ...

(和go的||不一样,不做惰性运算,所有参数都会执行)

比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。它们实现了go语言规则的值的比较,但具体的类型和大小会忽略掉,因此 任意类型有符号整数值都可以互相比较;任意类型无符号整数值都可以互相比较;等等。但是,整数和浮点数不能互相比较。

1.4.3 自定义函数

使用 Funcs 方法,可以将自定义好的函数放入到模板中。

Funcs 的签名:

func (t *Template) Funcs(funcMap FuncMap) *Template

Funcs方法向模板t的函数字典里加入参数funcMap内的键值对。

如果funcMap某个键值对的值不是函数类型或者返回值不符合要求会panic。

但是,可以对t函数列表的成员进行重写。方法返回t以便进行链式调用。

例子:

例子中的使用的一些方法,见第2部分

// 1. 定义函数,首字母可以小写,注意返回值
func SayHi(char string) (string, error) {
	return "Hi" + char, nil

}

func indexFunc(w http.ResponseWriter, r *http.Request) {
	// 2. new
	t := template.New("hello.tpl")
	// 3. 加入t的函数列表,需要替换掉t
	t = t.Funcs(template.FuncMap{"sayHi": SayHi})
	// 4. Parse 可以是文件也可以是字符串
	t, _ = t.ParseFiles("./hello.tpl")

	userName := "xxx"
	// 5. 渲染
	_ = t.Execute(w, userName)
}

上面代码的2-4步,可以使用一段链式调用完成:

	t, _ := template.New("hello.tpl").Funcs(template.FuncMap{"sayHi": SayHi}).ParseFiles("./hello.tpl")

注意事项

template.New 的文件名应该和要渲染的文件名一样

自定义函数有1-2个返回值,第一个值当做正式返回值。假如有第二个返回值:用来 panic,其类型必须是 error,当对应的值非 nil 时, panic

2 一些常用的方法

模板引擎的使用,一般有如下三步:

  1. 定义模板文件
  2. 解析模板文件
  3. 模板渲染

其中,第2、3步都要用到一些template的方法(这里用的是text/template)

2.1 解析模板文件的方法

// 解析字符串
func (t *Template) Parse(src string) (*Template, error)

// 解析1个或多个文件
func ParseFiles(filenames ...string) (*Template, error)

// 解析用正则匹配到的文件
func ParseGlob(pattern string) (*Template, error)

使用

1. Parse

这里使用 New 函数:

func New(name string) *Template

其作用是创建一个名为name的模板。

t, _ := template.New("test.tpl").Parse("<h1>{{ . }}</h1>")

Parse可以多次调用,但只有第一次调用可以包含空格、注释和模板定义之外的文本。

如果后面的调用在解析后仍剩余文本会引发错误、返回nil且丢弃剩余文本;

如果解析得到的模板已有相关联的同名模板,会覆盖掉原模板。

2. ParseFiles

t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl", "./h3.tpl")

解析匹配参数中的文件里的模板定义并将解析结果与t关联。

如果发生错误,会停止解析并返回nil,否则返回(t, nil)。至少要存在一个匹配的文件。

3. ParseGlob

t, _ := template.ParseGlob("./*.tpl")

解析当前目录下,所有以 .tpl 结尾的文件,假如有专门的文件夹存放模板文件,可以使用 templates/*.tmpl(1层目录时)和 templates/**/*.tmpl(2层目录时)

匹配时,和 ParseFiles 一样。

2.2 模板渲染的方法

func (t *Template) Execute(wr io.Writer, data interface{}) error
// Execute方法将解析好的模板应用到data上,并将输出写入wr。
// 如果执行时出现错误,会停止执行,但有可能已经写入wr部分数据。
// 模板可以安全的并发执行。

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
// 类似Execute,但是使用名为name的t关联的模板产生输出。

Execute 渲染的是 ParseFilesParseGlob 得到的第一个文件,假如要读取多个文件时,就有可能渲染的不是想要的文件,所以需要使用 ExecuteTemplate 指定一个已经解析的文件。

如:

t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
userName := "xxx"
_ = t.Execute(w, userName)

怎么样都是渲染 h1.tpl,假如要渲染 h2.tpl:

t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
userName := "xxx"
_ = t.ExecuteTemplate(w, "h2.tpl", userName)

注意

ExecuteTemplate 的name可以是 define 的模块名

如:

t, _ := template.New("test").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
_ = t.ExecuteTemplate(out, "T", "word")

当然,用其他解析方法也可以。

3 html/template的不同之处

由于 html/template 的API和 text/template 的API是一样的,解析和渲染没有什么不一样,但是在定义模板时,考虑到网站的安全性,会对一些风险内容进行转义,因此会有 text/template 有点差别。

如:

t, _ := template.New("test").Parse("{{ . }}")
char := "<script>alert('you have been pwned')</script>!"
_ = t.Execute(w, char)

得到的结果是: &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

与预期不符,为此, html/template 有一个函数可以专门处理这些我们认为安全的字符串: template.HTML

再使用时,我们可以自定义一个 safe 函数,和其他模板引擎一样,不对一些字符串转义。

func safe(s string) template.HTML {
	return template.HTML(s)
}

然后使用:

t, _ := template.New("test").Funcs(template.FuncMap{"safe": safe}).Parse("{{ . | safe }}")

_ = t.Execute(w, char)

补充

如果 {{.}}是非字符串类型的值,可以用于JavaScript上下文环境里:

struct{A,B string}{ "foo", "bar" }

将该值应用在在转义后的模板里:

<script>var pair = {{.}};</script>

模板输出为:

<script>var pair = {"A": "foo", "B": "bar"};</script>

参考:

  1. https://studygolang.com/static/pkgdoc/pkg/text_template.htm
  2. https://www.liwenzhou.com/posts/Go/go_template/
0  赞