模板引擎是 Web 編程中必不可少的一個組件。模板能分離邏輯和數據,使得邏輯簡潔清晰,而且模板可複用。引用第二篇文章《程序結構》一文中的圖示,咱們能夠看到模板引擎在 Web 程序結構中的位置:html
模板引擎按照功能能夠劃分爲兩種類型:git
這兩類模板引擎都比較極端。無邏輯模板引擎須要在處理器中額外添加不少邏輯用於生成替換的文本。而嵌入邏輯模板引擎則在模板中混入了大量邏輯,致使維護性較差。實用的模板引擎通常介於這二者之間。github
在Go 語言中,text/template
和html/template
兩個庫實現模板功能。golang
模板內容能夠是 UTF-8 編碼的任何內容。其中用{{
和}}
包圍的部分稱爲動做,{{}}
外的其它文本在輸出保持不變。模板須要應用到數據,模板中的動做會根據數據生成響應的內容來替換。web
模板解析以後能夠屢次執行,也能夠並行執行,可是注意使用同一個Writer
會致使輸出交替出現。編程
模板的內容較多,我將分爲兩篇文章介紹。本文介紹text/template
,包括 Go 模板的基本概念,用法和注意點。下篇文章介紹html/template
。數組
使用模板引擎通常有 3 個步驟:微信
text/template
或html/template
中的方法解析);package main import ( "log" "os" "text/template" ) type User struct { Name string Age int } func stringLiteralTemplate() { s := "My name is {{ .Name }}. I am {{ .Age }} years old.\n" t, err := template.New("test").Parse(s) if err != nil { log.Fatal("Parse string literal template error:", err) } u := User{Name: "darjun", Age: 28} err = t.Execute(os.Stdout, u) if err != nil { log.Fatal("Execute string literal template error:", err) } } func fileTemplate() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse file template error:", err) } u := User{Name: "dj", Age: 18} err = t.Execute(os.Stdout, u) if err != nil { log.Fatal("Execute file template error:", err) } } func main() { stringLiteralTemplate() fileTemplate() }
在可執行程序目錄中新建模板文件test
,並寫入下面的內容:編程語言
My name is {{ .Name }}. I am {{ .Age }} years old.
首先調用template.New
建立一個模板,參數爲模板名。函數
而後調用Template
類型的Parse
方法,解析模板字符串,生成模板主體。這個方法返回兩個值。若是模板語法正確,則返回模板對象自己和一個 nil 值。
若是有語法錯誤,則返回一個 error 類型的值做爲第二個返回值,這時不該該使用第一個返回值。
最後,調用模板對象的Execute
方法,傳入參數。Execute
執行模板中的動做,將結果輸出到os.Stdout
,即標準輸出。最終咱們看到模板中{{ .Name }}
被u
的Name
字段替換,{{ .Age }}
被u
的Age
字段替換,標準輸出中顯示下面一行字符串:
My name is darjun. I am 28 years old.
上面代碼中,fileTemplate
函數還演示瞭如何從文件中加載模板。其中template.ParseFiles
方法會建立一個模板,並將用戶指定的模板文件名用做這個新模板的名字:
t, err := template.ParseFiles("test")
至關於:
t := template.New("test") t, err := t.ParseFiles("test")
Go 模板中的動做就是一些嵌入在模板裏面的命令。動做大致上能夠分爲如下幾種類型:
在介紹其它的動做以前,咱們先看一個很重要的動做,點動做({{ . }}
)。它其實表明是傳遞給模板的數據,其餘動做或函數基本上都是對這個數據進行處理,以此來達到格式化和內容展現的目的。
對前面的代碼示例稍做修改:
func main() { s := "The user is {{ . }}." t, err := template.New("test").Parse(s) if err != nil { log.Fatal("Parse error:", err) } u := User{Name: "darjun", Age: 28} err = t.Execute(os.Stdout, u) if err != nil { log.Fatal("Execute error:", err) } }
運行程序,標準輸出顯示:
The user is {darjun 28}.
實際上,{{ . }}
會被替換爲傳給給模板的數據的字符串表示。這個字符串與以數據爲參數調用fmt.Sprint
函數獲得的內容相同。咱們能夠爲User
結構編寫一個方法:
func (u User) String() string { return fmt.Sprintf("(name:%s age:%d)", u.Name, u.Age) }
這樣替換的字符串就是格式化以後的內容了:
The user is (name:darjun age:28).
注意:爲了使用的方便和靈活,在模板中不一樣的上下文內,.
的含義可能會改變,下面在介紹不一樣的動做時會進行說明。
在介紹動做的語法時,我採用 Go 標準庫中的寫法。我以爲這樣寫更嚴謹。
其中pipeline
表示管道,後面會有詳細的介紹,如今能夠將它理解爲一個值。T1/T2
等形式表示語句塊,裏面能夠嵌套其它類型的動做。最簡單的語句塊就是不包含任何動做的字符串。
條件動做的語法與編程語言中的if
語句語法相似,有幾種形式:
形式一:
{{ if pipeline }} T1 {{ end }}
若是管道計算出來的值不爲空,執行T1
。不然,不生成輸出。下面都表示空值:
false
、0、空指針或接口;形式二:
{{ if pipeline }} T1 {{ else }} T2 {{ end }}
若是管道計算出來的值不爲空,執行T1
。不然,執行T2
。
形式三:
{{ if pipeline1 }} T1 {{ else if pipeline2 }} T2 {{ else }} T3 {{ end }}
若是管道pipeline1
計算出來的值不爲空,則執行T1
。反之若是管道pipeline2
的值不爲空,執行T2
。若是都爲空,執行T3
。
舉個栗子:
type AgeInfo struct { Age int GreaterThan60 bool GreaterThan40 bool } func main() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse error:", err) } rand.Seed(time.Now().Unix()) age := rand.Intn(100) info := AgeInfo { Age: age, GreaterThan60: age > 60, GreaterThan40: age > 40, } err = t.Execute(os.Stdout, info) if err != nil { log.Fatal("Execute error:", err) } }
在可執行程序的目錄下新建模板文件test
,鍵入下面的內容:
Your age is: {{ .Age }} {{ if .GreaterThan60 }} Old People! {{ else if .GreaterThan40 }} Middle Aged! {{ else }} Young! {{ end }}
運行程序,會隨機一個年齡,而後根據年齡區間選擇性輸出Old People!/Middle Age!/Young!
其中一個。下面是我運行兩次運行的輸出:
Your age is: 7 Young!
Your age is: 79 Old People!
這個程序有一個問題,會有多餘的空格!咱們以前說過,除了動做以外的任何文本都會原樣保持,包括空格和換行!針對這個問題,有兩種解決方案。第一種方案是刪除多餘的空格和換行,test
文件修改成:
Your age is: {{ .Age }} {{ if .GreaterThan60 }}Old People!{{ else if .GreaterThan40 }}Middle Aged!{{ else }}Young!{{ end }}
顯然,這個方法會致使模板內容很難閱讀,不夠理想。爲此,Go 提供了針對空白符的處理。若是一個動做以{{-
(注意有一個空格),那麼該動做與它前面相鄰的非空文本或動做間的空白符將會被所有刪除。相似地,若是一個動做以 -}}
結尾,那麼該動做與它後面相鄰的非空文本或動做間的空白符將會被所有刪除。例如:
{{23 -}} < {{- 45}}
將會生成輸出:
23<45
回到咱們的例子中,咱們能夠將test
文件稍做修改:
Your age is: {{ .Age }} {{ if .GreaterThan60 -}} "Old People!" {{- else if .GreaterThan40 -}} "Middle Aged!" {{- else -}} "Young!" {{- end }}
這樣,輸出的文本就不會包含多餘的空格了。
迭代其實與編程語言中的循環遍歷相似。有兩種形式:
形式一:
{{ range pipeline }} T1 {{ end }}
管道的值類型必須是數組、切片、map、channel。若是值的長度爲 0,那麼無輸出。不然,.
被設置爲當前遍歷到的元素,而後執行T1
,即在T1
中.
表示遍歷的當前元素,而非傳給模板的參數。若是值是 map 類型,且鍵是可比較的基本類型,元素將會以鍵的順序訪問。
形式二:
{{ range pipeline }} T1 {{ else }} T2 {{ end }}
與前一種形式基本同樣,若是值的長度爲 0,那麼執行T2
。
舉個栗子:
type Item struct { Name string Price int } func main() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse error:", err) } items := []Item { { "iPhone", 5499 }, { "iPad", 6331 }, { "iWatch", 1499 }, { "MacBook", 8250 }, } err = t.Execute(os.Stdout, items) if err != nil { log.Fatal("Execute error:", err) } }
在可執行程序目錄下新建模板文件test
,鍵入內容:
Apple Products: {{ range . }} {{ .Name }}: ¥{{ .Price }} {{ else }} No Products!!! {{ end }}
運行程序,獲得下面的輸出:
Apple Products: iPhone: ¥5499 iPad: ¥6331 iWatch: ¥1499 MacBook: ¥8250
在range
語句循環體內,.
被設置爲當前遍歷的元素,能夠直接使用{{ .Name }}
或{{ .Price }}
訪問產品名稱和價格。在程序中,將nil
傳給Execute
方法會獲得下面的輸出:
Apple Products: No Products!!!
設置動做使用with
關鍵字重定義.
。在with
語句內,.
會被定義爲指定的值。通常用在結構嵌套很深時,能起到簡化代碼的做用。
形式一:
{{ with pipeline }} T1 {{ end }}
若是管道值不爲空,則將.
設置爲pipeline
的值,而後執行T1
。不然,不生成輸出。
形式二:
{{ with pipeline }} T1 {{ else }} T2 {{ end }}
與前一種形式的不一樣之處在於當管道值爲空時,不改變.
執行T2
。舉個栗子:
type User struct { Name string Age int } type Pet struct { Name string Age int Owner User } func main() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse error:", err) } p := Pet { Name: "Orange", Age: 2, Owner: User { Name: "dj", Age: 28, }, } err = t.Execute(os.Stdout, p) if err != nil { log.Fatal("Execute error:", err) } }
模板文件內容:
Pet Info: Name: {{ .Name }} Age: {{ .Age }} Owner: {{ with .Owner }} Name: {{ .Name }} Age: {{ .Age }} {{ end }}
運行程序,獲得下面的輸出:
Pet Info: Name: Orange Age: 2 Owner: Name: dj Age: 28
可見,在with
語句內,.
被替換成了Owner
字段的值。
包含動做能夠在一個模板中嵌入另外一個模板,方便模板的複用。
形式一:
{{ template "name" }}
形式二:
{{ template "name" pipeline }}
其中name
表示嵌入的模板名稱。第一種形式,將使用nil
做爲傳入內嵌模板的參數。第二種形式,管道pipeline
的值將會做爲參數傳給內嵌的模板。舉個栗子:
package main import ( "log" "os" "text/template" ) func main() { t, err := template.ParseFiles("test1", "test2") if err != nil { log.Fatal("Parse error:", err) } err = t.Execute(os.Stdout, "test data") if err != nil { log.Fatal("Execute error:", err) } }
ParseFiles
方法接收可變參數,可將任意多個文件名傳給該方法。
模板test1
:
This is in test1. {{ template "test2" }} {{ template "test2" . }}
模板test2
:
This is in test2. Get: {{ . }}.
運行程序獲得輸出:
This is in test1. This is in test2. Get: <no value>. This is in test2. Get: test data.
前一個嵌入模板,沒有傳遞參數。後一個傳入.
,即傳給test1
模板的參數。
在介紹了幾種動做以後,咱們回過頭來看幾種基本組成部分。
註釋只有一種語法:
{{ /* 註釋 */ }}
註釋的內容不會呈如今輸出中,它就像代碼註釋同樣,是爲了讓模板更易讀。
一個參數就是模板中的一個值。它的取值有多種:
Execute
方法執行終止,返回該錯誤給調用者;上面幾種形式能夠結合使用:
{{ .Field1.Key1.Method1.Field2.Key2.Method2 }
其實,咱們已經用過不少次參數了。下面看一個方法調用的栗子:
type User struct { FirstName string LastName string } func (u User) FullName() string { return u.FirstName + " " + u.LastName } func main() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse error:", err) } err = t.Execute(os.Stdout, User{FirstName: "lee", LastName: "darjun"}) if err != nil { log.Fatal("Execute error:", err) } }
模板文件test
:
My full name is {{ .FullName }}.
模板執行會使用FullName
方法的返回值替換{{ .FullName }}
,輸出:
My full name is lee darjun.
關於參數的幾個要點:
{{ .Method1 }}
,若是Method1
方法返回一個函數,那麼返回值函數不會調用。若是要調用它,使用內置的call
函數。管道的語法與 Linux 中的管道相似,即命令的鏈式序列:
{{ p1 | p2 | p3 }}
每一個單獨的命令(即p1/p2/p3...
)能夠是下面三種類型:
在一個鏈式管道中,每一個命令的結果會做爲下一個命令的最後一個參數。最後一個命令的結果做爲整個管道的值。
管道必須只返回一個值,或者只返回一個值和一個錯誤。若是返回了非空的錯誤,那麼Execute
方法執行終止,並將該錯誤返回給調用者。
在迭代程序的基礎上稍做修改:
type Item struct { Name string Price float64 Num int } func (item Item) Total() float64 { return item.Price * float64(item.Num) } func main() { t, err := template.ParseFiles("test") if err != nil { log.Fatal("Parse error:", err) } item := Item {"iPhone", 5499.99, 2 } err = t.Execute(os.Stdout, item) if err != nil { log.Fatal("Execute error:", err) } }
模板文件test
:
Product: {{ .Name }} Price: ¥{{ .Price }} Num: {{ .Num }} Total: ¥{{ .Total | printf "%.2f" }}
先調用Item.Total
方法計算商品總價,而後使用printf
格式化,保留兩位小數。最終輸出:
Product: iPhone Price: ¥5499.99 Num: 2 Total: ¥10999.98
printf
是 Go 模板內置的函數,這樣的函數還有不少。
在動做中,能夠用管道的值定義一個變量。
$variable := pipeline
$variable
爲變量名,聲明變量的動做不生成輸出。
相似地,變量也能夠從新賦值:
$variable = pipeline
在range
動做中能夠定義兩個變量:
range $index, $element := range pipeline
這樣就能夠在循環中經過$index
和$element
訪問索引和元素了。
變量的做用域持續到定義它的控制結構的{{ end }}
動做。若是沒有這樣的控制結構,則持續到模板結束。模板調用不繼承變量。
執行開始時,$
被設置爲傳入的數據參數,即.
的值。
Go 模板提供了大量的預約義函數,若是有特殊需求也能夠實現自定義函數。模板執行時,遇到函數調用,先從模板自定義函數表中查找,然後查找全局函數表。
預約義函數分爲如下幾類:
and/or/not
;call
;print/printf/println
,與用參數直接調用fmt.Sprint/Sprintf/Sprintln
獲得的內容相同;eq/ne/lt/le/gt/ge
。在上面條件動做的示例代碼中,咱們在代碼中計算出大小關係再傳入模板,這樣比較繁瑣,能夠直接使用比較運算簡化。
有兩點須要注意:
默認狀況下,模板中無自定義函數,可使用模板的Funcs
方法添加。下面咱們實現一個格式化日期的自定義函數:
package main import ( "log" "os" "text/template" "time" ) func formatDate(t time.Time) string { return t.Format("2016-01-02") } func main() { funcMap := template.FuncMap { "fdate": formatDate, } t := template.New("test").Funcs(funcMap) t, err := t.ParseFiles("test") if err != nil { log.Fatal("Parse errr:", err) } err = t.Execute(os.Stdout, time.Now()) if err != nil { log.Fatal("Exeute error:", err) } }
模板文件test
:
Today is {{ . | fdate }}.
模板的Func
方法接受一個template.FuncMap
類型變量,鍵爲函數名,值爲實際定義的函數。
能夠一次設置多個自定義函數。自定義函數要求只返回一個值,或者返回一個值和一個錯誤。
設置以後就能夠在模板中使用fdate
了,輸出:
Today is 7016-01-07.
這裏不能使用template.ParseFiles
,由於在解析模板文件的時候fdate
未定義會致使解析失敗。必須先建立模板,調用Funcs
設置自定義函數,而後再解析模板。
咱們前面學習了兩種模板的建立方式:
template.New
建立模板,而後使用Parse/ParseFiles
解析模板內容;template.ParseFiles
建立並解析模板文件。第一種方式,調用template.New
建立模板時須要傳入一個模板名字,後續調用ParseFiles
能夠傳入一個或多個文件,這些文件中必須有一個基礎名(即去掉路徑部分)與模板名相同。若是沒有文件名與模板名相同,則Execute
調用失敗,返回錯誤。例如:
package main import ( "log" "os" "text/template" ) func main() { t := template.New("test") t, err := t.ParseFiles("test1") if err != nil { log.Fatal("Parse error:", err) } err = t.Execute(os.Stdout, nil) if err != nil { log.Fatal("Execute error:", err) } }
上面代碼先建立模板test
,而後解析文件test1
。執行該程序會出現下面的錯誤:
Execute error:template: test: "test" is an incomplete or empty template
Why?
咱們先來看看模板的結構:
// src/text/template.go 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 } type Template struct { name string *parse.Tree *common leftDelim string rightDelim string }
模板結構Template
中有一個字段common
,common
中又有一個字段tmpl
保存名字到模板的映射。其實,最外層的Template
結構是主模板,咱們調用Execute
方法時執行的就是主模板。
執行ParseFiles
方法時,每一個文件都會生成一個模板。只有文件基礎名與模板名相同時,該文件的內容纔會解析到主模板中。這也是上面的程序執行失敗的緣由——主模板爲空。
其它文件解析生成關聯模板,存儲在字段tmpl
中。關聯模板能夠是在主模板中經過{{ define }}
動做定義,或者在非主模板文件中定義。關聯模板也能夠執行,可是須要使用ExecuteTemplate
方法,顯式傳入模板名:
func main() t := template.New("test") t, err := t.ParseFiles("test1") if err != nil { log.Fatal("in associatedTemplate Parse error:", err) } err = t.ExecuteTemplate(os.Stdout, "test1", nil) if err != nil { log.Fatal("in associatedTemplate Execute error:", err) } }
第二種方式將建立和解析兩步合併在一塊兒了。template.ParseFiles
方法將傳入的第一個文件名做爲模板名稱,其他的文件(若是有的話)解析後存放在tmpl
中。
t, err := template.ParseFiles("file1", "file2", "file3")
其實就等價於:
t := template.New("file1") t, err := t.ParseFiles("file1", "file2", "file3")
少了不一致的可能性,因此調用Execute
方法時不會出現上面的錯誤。
還有一種建立方式,使用ParseGlob
函數。ParseGlob
會對匹配給定模式的全部文件進行語法分析。
func main() { t, err := template.ParseGlob("tmpl*.glob") if err != nil { log.Fatal("in globTemplate parse error:", err) } err = t.Execute(os.Stdout, nil) if err != nil { log.Fatal(err) } for i := 1; i <= 3; i++ { err = t.ExecuteTemplate(os.Stdout, fmt.Sprintf("tmpl%d.glob", i), nil) if err != nil { log.Fatal(err) } } }
ParseGlob
返回的模板以匹配的第一個文件基礎名做爲名稱。ParseGlob
解析時會對同一個目錄下的文件進行排序,因此第一個文件老是固定的。
咱們建立三個模板文件,tmpl1.glob
:
In glob template file1.
tmpl2.glob
:
In glob template file2.
tmpl3.glob
:
In glob template file3.
最終輸出爲:
In glob template file1. In glob template file1. In glob template file2. In glob template file3.
注意,若是多個不一樣路徑下的文件名相同,那麼後解析的會覆蓋以前的。
在一個模板文件中還能夠經過{{ define }}
動做定義其它的模板,這些模板就是嵌套模板。模板定義必須在模板內容的最頂層,像 Go 程序中的全局變量同樣。
嵌套模板通常用於佈局(layout)。不少文本的結構其實很是固定,例如郵件有標題和正文,網頁有首部、正文和尾部等。
咱們能夠爲這些固定結構的每部分定義一個模板。
定義模板文件layout.tmpl
:
{{ define "layout" }} This is body. {{ template "content" . }} {{ end }} {{ define "content" }} This is {{ . }} content. {{ end }}
上面定義了兩個模板layout
和content
,layout
中使用了content
。執行這種方式定義的模板必須使用ExecuteTemplate
方法:
func main() { t, err := template.ParseFiles("layout.tmpl") if err != nil { log.Fatal("Parse error:", err) } err = t.ExecuteTemplate(os.Stdout, "layout", "amazing") if err != nil { log.Fatal("Execute error:", err) } }
嵌套模板在網頁佈局中應用很是普遍,下一篇文章介紹html/template
時還會講到。
塊動做其實就是定義一個默認模板,語法以下:
{{ block "name" arg }} T1 {{ end }}
其實它就等價於定義一個模板,而後當即使用它:
{{ define "name" }} T1 {{ end }} {{ template "name" arg }}
若是後面定義了模板content
,那麼使用後面的定義,不然使用默認模板。
例如上面的示例中,咱們將模板修改以下:
{{ define "layout" }} This is body. {{ block "content" . }} This is default content. {{ end }} {{ end }}
去掉後面的content
模板定義,執行layout
時,content
部分會顯示默認值。
本文介紹了 Go 提供的模板text/template
。模板比較簡單易用,對於一些細節須要多加留意。代碼在Github上。
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~