本文只关注Go text/template的底层结构,带上了很详细的图片以及示例帮助理解,有些地方也附带上了源码进行解释。有了本文的解释,对于Go template的语法以及html/template的用法,一切都很简单。
入门示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
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。
例如:
1 2
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结构:
1 2 3 4 5 6 7
type Template struct { name string *parse.Tree *common leftDelim string rightDelim string }
name是这个Template的名称,Tree是解析树,common是另一个结构,稍后解释。leftDelim和rightDelim是左右两边的分隔符,默认为{{
和}}
。
这里主要关注name和common两个字段,name字段没什么解释的。common是一个结构:
1 2 3 4 5 6 7
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,也就是模板组。
1 2 3 4 5 6 7
func New(name string) *Template { t := &Template{ name: name, } t.init() return t }
其中t为模板的关联名称,name为模板的名称,t.init()表示如果模板对象t还没有common结构,就构造一个新的common组:
1 2 3 4 5 6 7 8 9
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。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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行的结果完全一致,所有行的第二个地址完全相同。
1 2 3 4 5 6
&{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个元素。