本文只關注Go text/template的底層結構,帶上了很詳細的圖片以及示例幫助理解,有些地方也附帶上了源碼進行解釋。有了本文的解釋,對於Go template的語法以及html/template的用法,一切都很簡單。html
關於template的語法以及具體使用方法,見:Go template用法詳解java
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。python
緊接着使用template.New()函數建立了一個空Template實例(對象),而後經過這個template實例調用Parse()方法,Parse()方法用來解析、評估模板中須要執行的action,其中須要評估的部分都使用{{}}
包圍,並將評估後(解析後)的結果賦值給tmpl。c++
最後調用Execute()方法,該方法將數據對象Person的實例p應用到已經解析的tmpl模板,最後將整個應用合併後的結果輸出到os.Stdout。函數
上面的示例很簡單,兩個注意點:this
{{}}
,其中使用了點(.),{{.Name}}
表明Execute()第二個參數p對象的Name字段,同理{{.Age}}
也就是說,{{.}}
表明的是要應用的對象,相似於java/c++中的this,python/perl中的self。code
更通用地,{{.}}
表示的是所處做用域的當前對象,而不只僅只表明Execute()中的第二個參數對象。例如,本示例中{{.}}
表明頂級做用域的對象p,若是Parse()中還有嵌套的做用域range,則{{.}}
表明range迭代到的每一個元素對象。若是瞭解perl語言,{{.}}
能夠理解爲默認變量$_
。htm
template中有很多函數、方法都直接返回*Template
類型。對象
上圖中使用紅色框線框起來一部分返回值是*Template
的函數、方法。對於函數,它們返回一個Template實例(假設爲t),對於使用t做爲參數的Must()函數和那些框起來的Template方法,它們返回的*Template
實際上是原始實例t。blog
例如:
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。
使用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分別爲T一、T2和T3,其中T1和T2嵌套在T3中,由於調用的是t1的Parse(),因此這3個新建立的模板都會關聯到t1上。
也就是說,如今t1上關聯了4個模板:test一、T一、T二、T3,它們全都共享同一個common。由於已經執行了Parse()解析操做,這個Parse()會將test一、T一、T二、T3的name添加到common.tmpl的map中。也就是說,common的tmpl字段的map結構中有4個元素。
結構以下圖:
必須注意,雖然test一、T一、T二、T3都關聯在t1上,但t1只能表明test1(因此上圖中只有test1下面標註了t1),由於t1是一個Template類型。能夠認爲test一、T一、T二、T3這4個模板共享一個組,但T一、T二、T3都是對外部不可見的,只能經過特殊方法的查詢找到它們。
另外,前文說過,template包中不少返回*Template
的函數、方法返回的實際上是原始的t(看源代碼便可知道),這個規則也適用於這裏的Parse()方法,因此tmpl和t1這兩個變量是徹底等價的,都指向同一個template,即test1。因此前面的執行結果中前3行徹底一致。
再回頭看上面代碼的執行結果,假設結果中的每一行都分爲3列,第一列爲template name,第二個字段爲parseTree的地址,第三列爲common結構的地址。由於tmpl一、t1都指向test1模板,因此前3行結果徹底一致。由於test一、T一、T二、T3共享同一個common,因此第三列全都相同。由於每一個模板的解析樹不同,因此第二列全都不同。
除了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()中定義了T一、T2和T3,t2.Parse()中定義了T四、T2和T3,且兩個T2的解析內容不同(解析樹不同)。
由於T一、T二、T三、T4都會加入到t1和t2共享的common中,因此不管是經過t1仍是經過t2這兩個關聯名稱都能找到T一、T二、T三、T4。可是後解析的會覆蓋先解析的,也就是說,不管是t1.Lookup("T2")
仍是t2.Lookup("T2")
獲得的T2對應的template,都是在t2.Parse()中定義的。當t1.Execute()
的時候,會獲得t2中定義的T2的值。
ONE TWOO ONE TWOO
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()根據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()
方法用於克隆一個徹底同樣的模板,包括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")) }
正常狀況下,不少函數、方法都返回兩個值,一個是想要返回的值,一個是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"))
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 }
這兩個方法均可以用來應用已經解析好的模板,應用表示對須要評估的數據進行操做,並和無需評估數據進行合併,而後輸出到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
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"。