推薦系統演算法工程師-從入門到就業
本文只關注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,則{{.}}
{{.}}
可以理解為預設變數$_
。
模板關聯(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個元素。