Go 学习笔记(三十九)深入剖析 Go template

本文原创地址:博客园骏马金龙Go 标准库:深入剖析 Go template

本文只关注 Go text/template 的底层结构,带上了很详细的图片以及示例帮助理解,有些地方也附带上了源码进行解释。有了本文的解释,对于 Go template 的语法以及 html/template 的用法,一切都很简单。

入门示例

package main


import (
	"fmt"
	"html/template"
	"os"
)


type Person struct {
	Name string
	Age    int
}

func main() {
	p := Person{"longshuai", 23}
	tmpl, err := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(os.Stdout, p)
	if err != nil {
		panic(err)
	}
	fmt.Println(tmpl)
}

输出:

Name: longshuai, Age: 23&{0xc000040260 0xc000018180 0xc000098000 0xc000096000}

上面定义了一个 Person 结构,有两个 大写字母开头 (意味着这俩字段是导出的) 的字段 Name 和 Age。然后 main() 中创建了 Person 的实例对象 p。

紧接着使用 template.New()函数创建了一个空 Template 实例 (对象),然后通过这个 template 实例调用 Parse()方法,Parse()方法用来解析、评估模板中需要执行的 action,其中需要评估的部分都使用{{}}包围,并将评估后 (解析后) 的结果赋值给 tmpl。

最后调用 Execute() 方法,该方法将数据对象 Person 的实例 p 应用到已经解析的 tmpl 模板,最后将整个应用合并后的结果输出到 os.Stdout。

上面的示例很简单,两个注意点:

  1. 流程:构建模板对象 New()--> 解析数据 Parse()--> 应用合并 Execute()
  2. Parse() 解析的对象中包含了{{}},其中使用了点 (.),{{.Name}}代表 Execute() 第二个参数 p 对象的 Name 字段,同理{{.Age}}

也就是说,{{.}}代表的是要应用的对象,类似于 java/c++ 中的 this,python/perl 中的 self。

更通用地,{{.}}表示的是所处作用域的当前对象,而不仅仅只代表 Execute() 中的第二个参数对象。例如,本示例中{{.}}代表顶级作用域的对象 p,如果 Parse() 中还有嵌套的作用域 range,则{{.}}代表 range 迭代到的每个元素对象。

模板关联 (associate)

template 中有不少函数、方法都直接返回*Template类型。

1.png

上图中使用红色框线框起来一部分返回值是*Template的函数、方法。对于函数,它们返回一个 Template 实例 (假设为 t),对于使用 t 作为参数的 Must() 函数和那些框起来的 Template 方法,它们返回的*Template 其实是原始实例 t

例如:

t := template.New("abc")
tt,err := t.Parse("xxxxxxxxxxx")

这里的 t 和 tt 其实都指向同一个模板对象。

这里的 t 称为模板的关联名称 。通俗一点,就是创建了一个模板,关联到变量 t 上。但 注意,t 不是模板的名称,因为 Template 中有一个未导出的 name 字段,它才是模板的名称 。可以通过 Name() 方法返回 name 字段的值,而且仔细观察上面的函数、方法,有些是以 name 作为参数的。

之所以要区分模板的关联名称 (t) 和模板的名称(name),是因为 一个关联名称 t(即模板对象) 上可以 "包含" 多个 name,也就是多个模板,通过 t 和各自的 name,可以调用到指定的模板

模板结构详解

首先看 Template 结构:

type Template struct {
	name string
	*parse.Tree
	*common
	leftDelim  string
	rightDelim string
}

name 是这个 Template 的名称,Tree 是解析树,common 是另一个结构,稍后解释。leftDelim 和 rightDelim 是左右两边的分隔符,默认为{{}}

这里主要关注 name 和 common 两个字段,name 字段没什么解释的。common 是一个结构:

type common struct {
	tmpl   map[string]*Template // Map from name to defined templates.
	option option
	muFuncs    sync.RWMutex // protects parseFuncs and execFuncs
	parseFuncs FuncMap
	execFuncs  map[string]reflect.Value
}

这个结构的第一个字段 tmpl 是一个 Template 的 map 结构,key 为 template 的 name,value 为 Template。也就是说,一个 common 结构中可以包含多个 Template,而 Template 结构中又指向了一个 common 结构。所以,common 是一个模板组,在这个模板组中的 (tmpl 字段) 所有 Template 都共享一个 common(模板组),模板组中包含 parseFuncs 和 execFuncs。

大概结构如下图:

2.png

除了需要关注的 name 和 common,parseFuncs 和 execFuncs 这两个字段也需要了解下,它们共同成为模板的 FuncMap。

New()函数和 init() 方法

使用 template.New() 函数可以创建一个空的、无解析数据的模板,同时还会创建一个 common,也就是模板组

func New(name string) *Template {
	t := &Template{
		name: name,
	}
	t.init()
	return t
}

其中 t 为模板的关联名称,name 为模板的名称,t.init() 表示如果模板对象 t 还没有 common 结构,就构造一个新的 common 组:

func (t *Template) init() {
	if t.common == nil {
		c := new(common)
		c.tmpl = make(map[string]*Template)
		c.parseFuncs = make(FuncMap)
		c.execFuncs = make(map[string]reflect.Value)
		t.common = c
	}
}

也就是说, template.New()函数不仅创建了一个模板,还创建了一个空的 common 结构 (模板组)。需要注意,新创建的 common 是空的,只有进行模板解析(Parse(),ParseFiles()等操作)之后,才会将模板添加到 common 的 tmpl 字段 (map 结构) 中

所以,下面的代码:

tmpl := template.New("mytmpl1")

执行完后将生成如下结构,其中 tmpl 为模板关联名称,mytmpl1 为模板名称。

3.png

因为还没有进行解析操作,所以上图使用虚线表示尚不存在的部分。

实际上,在 template 包中,很多涉及到操作 Template 的函数、方法,都会调用 init()方法保证返回的 Template 都有一个有效的 common 结构。当然,因为 init() 方法中进行了判断,对于已存在 common 的模板,不会新建 common 结构。

假设现在执行了 Parse() 方法,将会把模板 name 添加到 common tmpl 字段的 map 结构中,其中模板 name 为 map 的 key,模板为 map 的 value。

例如:

func main() {
	t1 := template.New("test1")
	tmpl,_ := t1.Parse(
			`{{define "T1"}}ONE{{end}}
			{{define "T2"}}TWO{{end}}
			{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
			{{template "T3"}}`)
	fmt.Println(t1)
	fmt.Println(tmpl)
	fmt.Println(t1.Lookup("test1"))  // 使用关联名称t1检索test1模板
	fmt.Println(t1.Lookup("T1"))
	fmt.Println(tmpl.Lookup("T2")) // 使用关联名称tmpl检索T2模板
	fmt.Println(tmpl.Lookup("T3"))
}

上述代码的执行结果:注意前 3 行的结果完全一致,所有行的第二个地址完全相同。

&{test1 0xc0420a6000 0xc0420640c0  }
&{test1 0xc0420a6000 0xc0420640c0  }
&{test1 0xc0420a6000 0xc0420640c0  }
&{T1 0xc0420a6100 0xc0420640c0  }
&{T2 0xc0420a6200 0xc0420640c0  }
&{T3 0xc0420a6300 0xc0420640c0  }

首先使用 template.New()函数创建了一个名为 test1 的模板,同时创建了一个模板组 (common),它们关联在 t1 变量上。

然后调用 Parse()方法,在 Parse() 的待解析字符串中使用 define 又定义了 3 个新的模板对象,模板的 name 分别为 T1、T2 和 T3,其中 T1 和 T2 嵌套在 T3 中,因为调用的是 t1 的 Parse(),所以这 3 个新创建的模板都会关联到 t1 上。

也就是说,现在 t1 上关联了 4 个模板:test1、T1、T2、T3,它们全都共享同一个 common。因为已经执行了 Parse()解析操作,这个 Parse() 会将 test1、T1、T2、T3 的 name 添加到 common.tmpl 的 map 中。也就是说,common 的 tmpl 字段的 map 结构中有 4 个元素。

结构如下图:

4.png

必须注意,虽然 test1、T1、T2、T3 都关联在 t1 上,但 t1 只能代表 test1(所以上图中只有 test1 下面标注了 t1),因为 t1 是一个 Template 类型。可以认为 test1、T1、T2、T3 这 4 个模板共享一个组,但 T1、T2、T3 都是对外部不可见的,只能通过特殊方法的查询找到它们。

另外,前文说过,template 包中很多返回*Template的函数、方法返回的其实是原始的 t(看源代码即可知道),这个规则也适用于这里的 Parse() 方法,所以 tmpl 和 t1 这两个变量是完全等价的,都指向同一个 template,即 test1。所以前面的执行结果中前 3 行完全一致。

再回头看上面代码的执行结果,假设结果中的每一行都分为 3 列, 第一列为 template name,第二个字段为 parseTree 的地址,第三列为 common 结构的地址 。因为 tmpl1、t1 都指向 test1 模板,所以前 3 行结果完全一致。因为 test1、T1、T2、T3 共享同一个 common,所以第三列全都相同。因为每个模板的解析树不一样,所以第二列全都不一样。

New() 方法

除了 template.New()函数,还有一个 Template.New() 方法:

// New allocates a new, undefined template associated with the given one and with the same
// delimiters. The association, which is transitive, allows one template to
// invoke another with a {{template}} action.
func (t *Template) New(name string) *Template {
	t.init()
	nt := &Template{
		name:       name,
		common:     t.common,
		leftDelim:  t.leftDelim,
		rightDelim: t.rightDelim,
	}
	return nt
}

看注释很难理解,但是看它的代码,结合前文的解释,New() 方法的作用很明显。

首先 t.init() 保证有一个有效的 common 结构,然后构造一个新的 Template 对象 nt,这个 nt 除了 name 和解析树 parse.Tree 字段之外,其它所有内容都和 t 完全一致。换句话说,nt 和 t 共享了 common。

也就是说,New() 方法使得名为 name 的 nt 模板对象加入到了关联组中。更通俗一点, 通过调用t.New()方法,可以创建一个新的名为 name 的模板对象,并将此对象加入到 t 模板组中

这和 New()函数的作用基本是一致的,只不过 New() 函数是构建新的模板对象并构建一个新的 common 结构,而 New() 方法则是构建一个新的模板对象,并加入到已有的 common 结构中。

只是还是要说明,因为 New()出来的新对象在执行解析之前 (如 Parse()),它们暂时都还不会加入到 common 组中,在 New() 出来之后,仅仅只是让它指向已有的一个 common 结构。

所以:

t1 := template.New("test1")
t1 = t1.Parse(...)
t2 := t1.New("test2")
t2 = t2.Parse(...)
t3 := t1.New("test3")

结构图:

5.png

如果 t1 和 t2 的 Parse() 中,都定义一个或多个 name 相同的模板会如何?例如:

t1 := template.New("test1")
t2 := t1.New("test2")
t1, _ = t1.Parse(
	`{{define "T1"}}ONE{{end}}
	{{define "T2"}}TWO{{end}}
	{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
	{{template "T3"}}`)
t2, _ = t2.Parse(
	`{{define "T4"}}ONE{{end}}
	{{define "T2"}}TWOO{{end}}
	{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
	{{template "T3"}}`)

	_ = t1.Execute(os.Stdout, "a")
	_ = t2.Execute(os.Stdout, "a")

在上面的 t1 和 t2 中,它们共享同一个 common,且 t1.Parse()中定义了 T1、T2 和 T3,t2.Parse() 中定义了 T4、T2 和 T3,且两个 T2 的解析内容不一样 (解析树不一样)。

因为 T1、T2、T3、T4 都会加入到 t1 和 t2 共享的 common 中,所以无论是通过 t1 还是通过 t2 这两个关联名称都能找到 T1、T2、T3、T4。但是后解析的会覆盖先解析的,也就是说,无论是t1.Lookup("T2")还是t2.Lookup("T2")得到的 T2 对应的 template,都是在 t2.Parse() 中定义的。当t1.Execute()的时候,会得到 t2 中定义的 T2 的值。

ONE TWOO
ONE TWOO

Parse()

Parse(string) 方法用于解析给定的文本内容 string。用法上很简单,前面也已经用过几次了,没什么可解释的。重点在于它的作用。

当创建了一个模板对象后,会有一个与之关联的 common(如果不存在,template 包中的各种函数、方法都会因为调用 init() 方法而保证 common 的存在 )。 只有在 Parse()之后,才会将相关的 template name 放进 common 中,表示这个模板已经可用了,或者称为已经定义了 (defined),可用被 Execute()或 ExecuteTemplate(),也表示可用使用 Lookup()和 DefinedTemplates()来检索模板 。另外,调用了 Parse() 解析后,会将给定的 FuncMap 中的函数添加到 common 的 FuncMap 中,只有添加到 common 的函数,才可以在模板中使用。

Parse()方法是解析字符串的,且只解析 New() 出来的模板对象。如果想要解析文件中的内容,见后文 ParseFiles()、ParseGlob()。

Lookup()、DefinedTemplates() 和 Templates() 方法

这三个方法都用于检索已经定义的模板,Lookup()根据 template name 来检索并返回对应的 template,DefinedTemplates() 则是返回所有已定义的 templates。Templates()和 DefinedTemplates() 类似,但是它返回的是[]*Template,也就是已定义的 template 的 slice。

前面多次说过,只有在解析之后,模板才加入到 common 结构中,才算是已经定义,才能被检索或执行。

当检索不存在的 templates 时,Lookup()将返回 nil。当 common 中没有模板,DefinedTemplates() 将返回空字符串 "",Templates() 将返回空的 slice。

func main() {
	t1 := template.New("test1")
	t2 := t1.New("test2")
	t1, _ = t1.Parse(
		`{{define "T1"}}ONE{{end}}
		{{define "T2"}}TWO{{end}}
		{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
		{{template "T3"}}`)
	t2, _ = t2.Parse(
		`{{define "T4"}}ONE{{end}}
		{{define "T2"}}TWOO{{end}}
		{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
		{{template "T3"}}`)

	fmt.Println(t1.DefinedTemplates())
	fmt.Println(t2.DefinedTemplates())
	fmt.Println(t2.Templates())
}

返回结果:

; defined templates are: "T1", "T2", "T3", "test1", "T4", "test2"
; defined templates are: "test1", "T4", "test2", "T1", "T2", "T3"
[0xc04201c280 0xc042064100 0xc04201c1c0 0xc04201c2c0 0xc04201c300 0xc042064080]

从结果可见,返回的顺序虽然不一致,但包含的 template name 是完全一致的。

Clone() 方法

Clone()方法用于克隆一个完全一样的模板,包括 common 结构也会完全克隆

t1 := template.New("test1")
t1 = t1.Parse(...)
t2 := t1.New("test2")
t2 = t2.Parse(...)

t3, err := t1.Clone()
if err != nil {
	panic(err)
}

这里的 t3 和 t1 在内容上完全一致,但在内存中它们是两个不同的对象。但无论如何,目前 t3 中会包含 t1 和 t2 共享的 common,即使 t2 中定义了{{define "Tx"}}...{{end}},这个 Tx 也会包含在 t3 中。

因为是不同的对象,所以修改 t3,不会影响 t1/t2。

看下面的例子:

func main() {
	t1 := template.New("test1")
	t2 := t1.New("test2")
	t1, _ = t1.Parse(
		`{{define "T1"}}ONE{{end}}
		{{define "T2"}}TWO{{end}}
		{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
		{{template "T3"}}`)
	t2, _ = t2.Parse(
		`{{define "T4"}}ONE{{end}}
		{{define "T2"}}TWOO{{end}}
		{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
		{{template "T3"}}`)

	t3, err := t1.Clone()
	if err != nil {
		panic(err)
	}

	// 结果完全一致
	fmt.Println(t1.Lookup("T4"))
	fmt.Println(t3.Lookup("T4"))
	
	// 修改t3
	t3,_ = t3.Parse(`{{define "T4"}}one{{end}}`)
	// 结果将不一致
	fmt.Println(t1.Lookup("T4"))
	fmt.Println(t3.Lookup("T4"))
}

Must() 函数

正常情况下,很多函数、方法都返回两个值,一个是想要返回的值,一个是 err 信息。template 包中的函数、方法也一样如此。

但有时候不想要 err 信息,而是直接取第一个返回值,并赋值给变量。操作大概是这样的:

t1 := template.New("ttt")
t1,err := t1.Parse(...)
if err != nil {
	panic(err)
}
...

Must()函数将上面的过程封装了,使得 Must() 可以简化上面的操作:

func Must(t *Template, err error) *Template {
	if err != nil {
		panic(err)
	}
	return t
}

当某个返回*Template,err的函数、方法需要直接使用时,可用将其包装在 Must() 中,它会自动在有 err 的时候 panic,无错的时候只返回其中的*Template

这在赋值给变量的时候非常简便,例如:

var t = template.Must(template.New("name").Parse("text"))

ParseFiles()和 ParseGlob()

Parse()只能解析字符串,要解析文件中的内容,需要使用 ParseFiles() 或 ParseGlob()。

template 包中有 ParseFiles()和 ParseGlob() 函数,也有 ParseFiles()和 ParseGlob() 方法。

6.png

这两个函数和这两个方法的区别,看一下文档就很清晰:

$ go doc template.ParseFiles
func ParseFiles(filenames ...string) (*Template, error)
	ParseFiles creates a new Template and parses the template definitions from
	the named files. The returned template's name will have the (base) name and
	(parsed) contents of the first file. There must be at least one file. If an
	error occurs, parsing stops and the returned *Template is nil.

$ go doc template.template.ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
	ParseFiles parses the named files and associates the resulting templates
	with t. If an error occurs, parsing stops and the returned template is nil;
	otherwise it is t. There must be at least one file.

解释很清晰。ParseFiles()函数是直接解析一个或多个文件的内容,并返回第一个文件名的 basename 作为 Template 的名称,也就是说这些文件的 template 全都关联到第一个文件的 basename 上。ParseFiles() 方法则是解析一个或多个文件的内容,并将这些内容关联到 t 上。

看示例就一目了然。

例如,当前 go 程序的目录下有 3 个文件:a.cnf、b.cnf 和 c.cnf,它们的内容无所谓,反正空内容也可以解析。

func main() {
	t1,err := template.ParseFiles("a.cnf","b.cnf","c.cnf")
	if err != nil {
		panic(err)
	}
	fmt.Println(t1.DefinedTemplates())
	fmt.Println()
	fmt.Println(t1)
	fmt.Println(t1.Lookup("a.cnf"))
	fmt.Println(t1.Lookup("b.cnf"))
	fmt.Println(t1.Lookup("c.cnf"))
}

输出结果:

; defined templates are: "a.cnf", "b.cnf", "c.cnf"

&{a.cnf 0xc0420ae000 0xc042064140  }
&{a.cnf 0xc0420ae000 0xc042064140  }
&{b.cnf 0xc0420bc000 0xc042064140  }
&{c.cnf 0xc0420bc100 0xc042064140  }

从结果中可以看到,已定义的 template name 都是文件的 basename,且 t1 和 a.cnf 这个 template 是完全一致的,即 t1 是文件列表中的第一个模板对象。

结构如下图:

7.png

理解了 ParseFiles()函数,理解 ParseFiles() 方法、ParseGlob()函数、ParseGlob() 方法,应该不会再有什么问题。但是还是有需要注意的地方:

func main() {
	t1 := template.New("test")
	t1,err := t1.ParseFiles("a.cnf","b.cnf","c.cnf")
	if err != nil {
		panic(err)
	}
	// 先注释下面这行
	//t1.Parse("")
	fmt.Println(t1.DefinedTemplates())
	fmt.Println()
	fmt.Println(t1)
	fmt.Println(t1.Lookup("a.cnf"))
	fmt.Println(t1.Lookup("b.cnf"))
	fmt.Println(t1.Lookup("c.cnf"))
}

执行结果:

; defined templates are: "a.cnf", "b.cnf", "c.cnf"

&{test <nil> 0xc0420640c0  }
&{a.cnf 0xc0420b0000 0xc0420640c0  }
&{b.cnf 0xc0420be000 0xc0420640c0  }
&{c.cnf 0xc0420be100 0xc0420640c0  }

发现 template.New() 函数创建的模板对象 test 并没有包含到 common 中。为什么?

因为 t.ParseFiles()、t.ParseGlob() 方法的解析过程是独立于 t 之外的,它们只解析文件内容,不解析字符串。而 New()出来的模板,需要 Parse() 方法来解析才会加入到 common 中。

将上面的注释行取消掉,执行结果将如下:

; defined templates are: "a.cnf", "b.cnf", "c.cnf", "test"

&{test 0xc0420bc200 0xc0420640c0  }
&{a.cnf 0xc0420ae000 0xc0420640c0  }
&{b.cnf 0xc0420bc000 0xc0420640c0  }
&{c.cnf 0xc0420bc100 0xc0420640c0  }

具体原因可分析 parseFiles() 源码:

func parseFiles(t *Template, filenames ...string) (*Template, error) {
	if len(filenames) == 0 {
		// Not really a problem, but be consistent.
		return nil, fmt.Errorf("template: no files named in call to ParseFiles")
	}
	for _, filename := range filenames {
		b, err := ioutil.ReadFile(filename)
		if err != nil {
			return nil, err
		}
		s := string(b)

		// name为文件名的basename部分
		name := filepath.Base(filename)

		var tmpl *Template
		if t == nil {
			t = New(name)
		}
		// 如果调用t.Parsefiles(),则t.Name不为空
		// name也就不等于t.Name
		// 于是新New(name)一个模板对象给tmpl
		if name == t.Name() {
			tmpl = t
		} else {
			tmpl = t.New(name)
		}
		// 解析tmpl。如果选中了上面的else分支,则和t无关
		_, err = tmpl.Parse(s)
		if err != nil {
			return nil, err
		}
	}
	return t, nil
}

Execute()和 ExecuteTemplate()

这两个方法都可以用来应用已经解析好的模板,应用表示对需要评估的数据进行操作,并和无需评估数据进行合并,然后输出到 io.Writer 中:

func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

两者的区别在于 Execute()是应用整个 common 中已定义的模板对象,而 ExecuteTemplate() 可以选择 common 中某个已定义的模板进行应用。

例如:

func main() {
	t1 := template.New("test1")
	t1, _ = t1.Parse(`{{define "T1"}}ONE{{end}}
		{{- define "T2"}}TWO{{end}}
		{{- define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
		{{- template "T3"}}`)
	
	_ = t1.Execute(os.Stdout,"")
	fmt.Println()
	fmt.Println("-------------")
	_ = t1.ExecuteTemplate(os.Stdout, "T2", "")
}

输出结果:

ONE TWO
-------------
TWO

FuncMap 和 Funcs()

template 内置了一系列函数,但这些函数毕竟有限,可能无法满足特殊的需求。template 允许我们定义自己的函数,添加到 common 中,然后就可以在待解析的内容中像使用内置函数一样使用自定义的函数。

自定义函数的优先级高于内置的函数优先级,即先检索自定义函数,再检索内置函数。也就是说,如果自定义函数的函数名和内置函数名相同,则内置函数将失效。

本文只对此稍作解释,本文的重点不是 template 的具体语法和用法。

在 common 结构中,有一个字段是 FuncMap 类型的:

type common struct {
	tmpl   map[string]*Template
	option option
	muFuncs    sync.RWMutex // protects parseFuncs and execFuncs
	parseFuncs FuncMap
	execFuncs  map[string]reflect.Value
}

这个类型的定义为:

type FuncMap map[string]interface{}

它是一个 map 结构,key 为模板中可以使用的函数名,value 为函数对象 (为了方便称呼,这里直接成为函数)。函数必须只有 1 个值或 2 个值,如果有两个值,第二个值必须是 error 类型的,当执行函数时 err 不为空,则执行自动停止。

函数可以有多个参数。假如函数 str 有两个参数,在待解析的内容中调用函数 str 时,如果调用方式为{{str . "aaa"}},表示第一个参数为当前对象,第二个参数为字符串 "aaa"。

假如,要定义一个将字符串转换为大写的函数,可以:

import "strings"
func upper(str string) string {
	return strings.ToUpper(str)
}

然后将其添加到 FuncMap 结构中,并将此函数命名为 "strupper",以后在待解析的内容中就可以调用 "strupper" 函数。

funcMap := template.FuncMap{
	"strupper": upper,
}

或者,直接将匿名函数放在 FuncMap 内部:

funcMap := template.FuncMap{
	"strupper": func(str string) string { return strings.ToUpper(str) },
}

现在只是定义了一个 FuncMap 实例,这个实例中有一个函数。还没有将它关联到模板,严格地说还没有将其放进 common 结构。要将其放进 common 结构,调用 Funcs()方法 ( 其实调用此方法也没有将其放进 common,只有在解析的时候才会放进 common):

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

例如:

funcMap := template.FuncMap{
	"strupper": func(str string) string { return strings.ToUpper(str) },
}
t1 := template.New("test")
t1 = t1.Funcs(funcMap)

这样,和 t1 共享 common 的所有模板都可以调用 "strupper" 函数。

注意,必须在解析之前调用 Funcs() 方法,在解析的时候会将函数放进 common 结构。

下面是完整的示例代码:

package main

import (
	"os"
	"strings"
	"text/template"
)

func main() {
	funcMap := template.FuncMap{
		"strupper": upper,
	}
	t1 := template.New("test1")
	tmpl, err := t1.Funcs(funcMap).Parse(`{{strupper .}}`)
	if err != nil {
		panic(err)
	}
	_ = tmpl.Execute(os.Stdout, "go programming")
}

func upper(str string) string {
	return strings.ToUpper(str)
}

上面调用了{{strupper .}},这里的 strupper 是我们自定义的函数,"." 是它的参数 (注意,参数不是放进括号里)。这里的 "." 代表当前作用域内的当前对象,对于这个示例来说,当前对象就是那段字符串对象 "go programming"。

上一篇 Go 学习笔记(三十八)数据存储 (3)——gob 对象序列化
Go 学习笔记(目录)
下一篇 Go 学习笔记(四十)Go template 用法详解