iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > GO >深入解析Gotemplate模板使用详解
  • 145
分享到

深入解析Gotemplate模板使用详解

2024-04-02 19:04:59 145人浏览 薄情痞子
摘要

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

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

关于template的语法以及具体使用方法,见:Go template用法详解

入门示例

package main

import (
	"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)
}

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

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

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

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

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

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

更通用地,{{.}}表示的是所处作用域的当前对象,而不仅仅只代表Execute()中的第二个参数对象。例如,本示例中{{.}}代表顶级作用域的对象p,如果Parse()中还有嵌套的作用域range,则{{.}}代表range迭代到的每个元素对象。如果了解perl语言,{{.}}可以理解为默认变量$_

模板关联(associate)

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

上图中使用红色框线框起来一部分返回值是*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。

大概结构如下图:

除了需要关注的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为模板名称。

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

实际上,在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个元素。

结构如下图:

必须注意,虽然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")

结构图:

如果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()方法。

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

$ 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是文件列表中的第一个模板对象。

结构如下图:

理解了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"。

 

您可能感兴趣的文档:

--结束END--

本文标题: 深入解析Gotemplate模板使用详解

本文链接: https://www.lsjlt.com/news/146316.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入解析Gotemplate模板使用详解
    本文只关注Go text/template的底层结构,带上了很详细的图片以及示例帮助理解,有些地方也附带上了源码进行解释。有了本文的解释,对于Go template的语法以及html...
    99+
    2024-04-02
  • C++深入了解模板的使用
    目录一.泛型编程二.函数模板1.函数模板概念2.函数模板格式3.函数模板的原理三.类模板一.泛型编程 泛型编程:不再是针对某种类型,能适应广泛的类型,跟具体的类型无关的代码 如何实现...
    99+
    2024-04-02
  • 攻克 VUE 模板语法难关:深入解析
    基本语法 Vue 模板语法基于 HTML,并使用特殊指令扩展了 HTML 功能。指令以 "v-" 前缀开头,用于绑定数据、处理事件和实现动态行为。 v-model "v-model" 指令实现双向数据绑定,允许在输入元素(如 <i...
    99+
    2024-03-04
    Vue 模板语法 v-model v-bind v-on 双向数据绑定 计算属性 watch
  • C++模板全方位深入解读
    目录1.泛型编程2.函数模板概念函数模板的格式函数模板的原理函数模板的实例化隐式实例化显式实例化模板参数的匹配原则3.类模板(1) 类模板的定义格式(2) 类模板的实例化4.非类型模...
    99+
    2024-04-02
  • Flask模板继承深入理解与应用
    目录什么叫模板继承呢模板页完整代码什么叫模板继承呢 在我的理解就是:在前端页面中肯定有很多页面中有很多相同的地方,比如页面顶部的导航栏,底部的页脚等部分,这时候如果每一个页面都重写一...
    99+
    2024-04-02
  • C++标准模板库STL深入讲解
    目录认识STLSTL的概述STL标准模板库都有什么容器算法迭代器函数符空间配置器string字符容器库vector容器vector容器于array数组容器的区别空间分配策略迭代器非法...
    99+
    2022-12-26
    C++标准模板库STL C++标准模板库 C++ STL
  • C++泛型模板约束深入讲解
    CPP参考:(新标准) 传送门 模板对于类型的约束: 约束 template_get_size 泛型T只允许接受类型:list<T>,其实为 C/C++ 泛型模板例化特性...
    99+
    2024-04-02
  • springboot中thymeleaf模板使用详解
    这篇文章将更加全面详细的介绍thymeleaf的使用。thymeleaf 是新一代的模板引擎,在spring4.0中推荐使用thymeleaf来做前端模版引擎。thymeleaf介绍简单说, Thymeleaf 是一个跟 Velocity、...
    99+
    2023-05-31
    springboot thymeleaf
  • Django模板继承与模板的导入实例详解
    目录一:模版的继承1.什么是模板继承2.使用继承流程原理3.模板继承语法二:模板的继承使用1.案例需求2.总结模板继承三:模版的导入1.模板导入2.模板导入格式3.模板导入使用4.使...
    99+
    2024-04-02
  • RxJava2Scheduler使用实例深入解析
    目录前言Scheduler 与 Worker定义scheduleDirect / schedulePeriodicallyDirectcreateWorkerSchedulersNe...
    99+
    2022-11-13
    RxJava Scheduler RxJava2 Scheduler使用
  • Python模板的使用详细讲解
    目录一 模板语法传值二 过滤器三 标签四 自定义模板标签和过滤器4.1 自定义过滤器4.2 自定义标签函数4.3 自定义inclusion_tag五 模板的继承六 模板的导入一 模板...
    99+
    2024-04-02
  • C++模板index_sequence使用示例详解
    目录引言integer_sequenceindex_sequencemake_index_sequence使用场景index_sequence_for结语引言 integer_se...
    99+
    2022-12-08
    C++模板index_sequence C++ 模板
  • C++函数模板的使用详解
    函数模板可以适用泛型来定义函数,其中泛型可以是(int, double, float)等替换。在函数重载过程中,通过将类型作为参数传递给模板,可使编译器自动产生该类型的函数。 工作原...
    99+
    2024-04-02
  • 深入理解Java设计模式之模板方法模式
    目录一、什么是模板方法模式二、模板方法模式的使用场景三、模板方法模式的优缺点四、模板方法模式的实现五、总结一、什么是模板方法模式 模板方法模式在一个方法中定义一个算法的骨架,而将一些...
    99+
    2024-04-02
  • 如何深入剖析C++类模板应用代码
    本篇文章给大家分享的是有关如何深入剖析C++类模板应用代码,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。C++中有类继承的概念,意思就是能够实现与继承相同功能的一种应用。C++...
    99+
    2023-06-17
  • 深入IDEADebug问题透析详解
    目录引言问题引入Debug对代码简单分析打断点断点分析条件断点单步调试重回代码分析总结引言 本来通过问题引入,透析 IDEA Debug。通过阅读本文可以学习如何通过 IDEA 的 ...
    99+
    2023-01-08
    IDEA Debug问题透析 IDEA Debug
  • 超详细的JVM 深入解析
    工作之余,想总结一下JVM相关知识。Java运行时数据区:Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、创建和销毁的时间,有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程...
    99+
    2023-06-02
  • GoJs面板绘图模板go.Panel使用示例详解
    目录前言go.Panel的使用go.Panel的类型go.panel.Auto类型go.panel.Grid类型go.panel.Graduated类型go.panel.Horizo...
    99+
    2023-05-16
    GoJs面板绘图go.Panel GoJs 面板绘图
  • 深入解析Canvas的渲染模式
    Canvas的renderMode详解,需要具体代码示例 在Unity中,Canvas是实现2D UI最基础和关键的组件。Canvas在渲染过程中有两种不同的模式:Screen Space和World Space。这些渲染模式在...
    99+
    2024-01-17
    Canvas 详解
  • C++超详细讲解模板的使用
    目录一、函数模板1.1函数模板概念1.2 函数模板格式1.3 函数模板的原理1.4 函数模板的实例化二、类模板2.1 类模板的定义格式2.2类模板的实例化总结一、函数模板 1.1函数...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作