## 什么是模板
你一定聽說過一種叫做MVC的設計模式,Model處理數據,View展現結果,Controller控制用戶的請求,至于View層的處理,在很多動態語言里面都是通過在靜態HTML中插入動態語言生成的數據,例如JSP中通過插入``,PHP中通過插入``來實現的。
通過下面這個圖可以說明模板的機制
[](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/images/7.4.template.png?raw=true)
圖7.1 模板機制圖
Web應用反饋給客戶端的信息中的大部分內容是靜態的,不變的,而另外少部分是根據用戶的請求來動態生成的,例如要顯示用戶的訪問記錄列表。用戶之間只有記錄數據是不同的,而列表的樣式則是固定的,此時采用模板可以復用很多靜態代碼。
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#go模板使用)Go模板使用
在Go語言中,我們使用`template`包來進行模板處理,使用類似`Parse`、`ParseFile`、`Execute`等方法從文件或者字符串加載模板,然后執行類似上面圖片展示的模板的merge操作。請看下面的例子:
~~~
func handler(w http.ResponseWriter, r *http.Request) {
t := template.New("some template") //創建一個模板
t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件
user := GetUser() //獲取當前用戶信息
t.Execute(w, user) //執行模板的merger操作
}
~~~
通過上面的例子我們可以看到Go語言的模板操作非常的簡單方便,和其他語言的模板處理類似,都是先獲取數據,然后渲染數據。
為了演示和測試代碼的方便,我們在接下來的例子中采用如下格式的代碼
* 使用Parse代替ParseFiles,因為Parse可以直接測試一個字符串,而不需要額外的文件
* 不使用handler來寫演示代碼,而是每個測試一個main,方便測試
* 使用`os.Stdout`代替`http.ResponseWriter`,因為`os.Stdout`實現了`io.Writer`接口
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#模板中如何插入數據)模板中如何插入數據?
上面我們演示了如何解析并渲染模板,接下來讓我們來更加詳細的了解如何把數據渲染出來。一個模板都是應用在一個Go的對象之上,Go對象的字段如何插入到模板中呢?
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#字段操作)字段操作
Go語言的模板通過`{{}}`來包含需要在渲染時被替換的字段,`{{.}}`表示當前的對象,這和Java或者C++中的this類似,如果要訪問當前對象的字段通過`{{.FieldName}}`,但是需要注意一點:這個字段必須是導出的(字段首字母必須是大寫的),否則在渲染的時候就會報錯,請看下面的這個例子:
~~~
package main
import (
"html/template"
"os"
)
type Person struct {
UserName string
}
func main() {
t := template.New("fieldname example")
t, _ = t.Parse("hello {{.UserName}}!")
p := Person{UserName: "Astaxie"}
t.Execute(os.Stdout, p)
}
~~~
上面的代碼我們可以正確的輸出`hello Astaxie`,但是如果我們稍微修改一下代碼,在模板中含有了未導出的字段,那么就會報錯
~~~
type Person struct {
UserName string
email string //未導出的字段,首字母是小寫的
}
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
~~~
上面的代碼就會報錯,因為我們調用了一個未導出的字段,但是如果我們調用了一個不存在的字段是不會報錯的,而是輸出為空。
如果模板中輸出`{{.}}`,這個一般應用于字符串對象,默認會調用fmt包輸出字符串的內容。
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#輸出嵌套字段內容)輸出嵌套字段內容
上面我們例子展示了如何針對一個對象的字段輸出,那么如果字段里面還有對象,如何來循環的輸出這些內容呢?我們可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`來進行數據的輸出。
* {{range}} 這個和Go語法里面的range類似,循環操作數據
* {{with}}操作是指當前對象的值,類似上下文的概念
詳細的使用請看下面的例子:
~~~
package main
import (
"html/template"
"os"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an email {{.}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
~~~
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#條件處理)條件處理
在Go模板里面如果需要進行條件判斷,那么我們可以使用和Go語言的`if-else`語法類似的方式來處理,如果pipeline為空,那么if就認為是false,下面的例子展示了如何使用`if-else`語法:
~~~
package main
import (
"os"
"text/template"
)
func main() {
tEmpty := template.New("template test")
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不會輸出. {{end}}\n"))
tEmpty.Execute(os.Stdout, nil)
tWithValue := template.New("template test")
tWithValue = template.Must(tWithValue.Parse("不為空的 pipeline if demo: {{if `anything`}} 我有內容,我會輸出. {{end}}\n"))
tWithValue.Execute(os.Stdout, nil)
tIfElse := template.New("template test")
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
tIfElse.Execute(os.Stdout, nil)
}
~~~
通過上面的演示代碼我們知道`if-else`語法相當的簡單,在使用過程中很容易集成到我們的模板代碼中。
> 注意:if里面無法使用條件判斷,例如.Mail=="[astaxie@gmail.com](mailto:astaxie@gmail.com)",這樣的判斷是不正確的,if里面只能是bool值
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#pipelines)pipelines
Unix用戶已經很熟悉什么是`pipe`了,`ls | grep "beego"`類似這樣的語法你是不是經常使用,過濾當前目錄下面的文件,顯示含有"beego"的數據,表達的意思就是前面的輸出可以當做后面的輸入,最后顯示我們想要的數據,而Go語言模板最強大的一點就是支持pipe數據,在Go語言里面任何`{{}}`里面的都是pipelines數據,例如我們上面輸出的email里面如果還有一些可能引起XSS注入的,那么我們如何來進行轉化呢?
~~~
{{. | html}}
~~~
在email輸出的地方我們可以采用如上方式可以把輸出全部轉化html的實體,上面的這種方式和我們平常寫Unix的方式是不是一模一樣,操作起來相當的簡便,調用其他的函數也是類似的方式。
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#模板變量)模板變量
有時候,我們在模板使用過程中需要定義一些局部變量,我們可以在一些操作中申明局部變量,例如`with``range``if`過程中申明局部變量,這個變量的作用域是`{{end}}`之前,Go語言通過申明的局部變量格式如下所示:
~~~
$variable := pipeline
~~~
詳細的例子看下面的:
~~~
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
~~~
### [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#模板函數)模板函數
模板在輸出對象的字段值時,采用了`fmt`包把對象轉化成了字符串。但是有時候我們的需求可能不是這樣的,例如有時候我們為了防止垃圾郵件發送者通過采集網頁的方式來發送給我們的郵箱信息,我們希望把`@`替換成`at`例如:`astaxie at beego.me`,如果要實現這樣的功能,我們就需要自定義函數來做這個功能。
每一個模板函數都有一個唯一值的名字,然后與一個Go函數關聯,通過如下的方式來關聯
~~~
type FuncMap map[string]interface{}
~~~
例如,如果我們想要的email函數的模板函數名是`emailDeal`,它關聯的Go函數名稱是`EmailDealWith`,那么我們可以通過下面的方式來注冊這個函數
~~~
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
~~~
`EmailDealWith`這個函數的參數和返回值定義如下:
~~~
func EmailDealWith(args …interface{}) string
~~~
我們來看下面的實現例子:
~~~
package main
import (
"fmt"
"html/template"
"os"
"strings"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func EmailDealWith(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
// find the @ symbol
substrs := strings.Split(s, "@")
if len(substrs) != 2 {
return s
}
// replace the @ by " at "
return (substrs[0] + " at " + substrs[1])
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an emails {{.|emailDeal}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
~~~
上面演示了如何自定義函數,其實,在模板包內部已經有內置的實現函數,下面代碼截取自模板包里面
~~~
var builtins = FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
}
~~~
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#must操作)Must操作
模板包里面有一個函數`Must`,它的作用是檢測模板是否正確,例如大括號是否匹配,注釋是否正確的關閉,變量是否正確的書寫。接下來我們演示一個例子,用Must來判斷模板是否正確:
~~~
package main
import (
"fmt"
"text/template"
)
func main() {
tOk := template.New("first")
template.Must(tOk.Parse(" some static text /* and a comment */"))
fmt.Println("The first one parsed OK.")
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
fmt.Println("The second one parsed OK.")
fmt.Println("The next one ought to fail.")
tErr := template.New("check parse error with Must")
template.Must(tErr.Parse(" some static text {{ .Name }"))
}
~~~
講輸出如下內容
~~~
The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command
~~~
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#嵌套模板)嵌套模板
我們平常開發Web應用的時候,經常會遇到一些模板有些部分是固定不變的,然后可以抽取出來作為一個獨立的部分,例如一個博客的頭部和尾部是不變的,而唯一改變的是中間的內容部分。所以我們可以定義成`header`、`content`、`footer`三個部分。Go語言中通過如下的語法來申明
~~~
{{define "子模板名稱"}}內容{{end}}
~~~
通過如下方式來調用:
~~~
{{template "子模板名稱"}}
~~~
接下來我們演示如何使用嵌套模板,我們定義三個文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的內容如下
~~~
//header.tmpl
{{define "header"}}
<html>
<head>
<title>演示信息</title>
</head>
<body>
{{end}}
//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>演示嵌套</h1>
<ul>
<li>嵌套使用define定義子模板</li>
<li>調用使用template</li>
</ul>
{{template "footer"}}
{{end}}
//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}
~~~
演示代碼如下:
~~~
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
s1.ExecuteTemplate(os.Stdout, "header", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "content", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "footer", nil)
fmt.Println()
s1.Execute(os.Stdout, nil)
}
~~~
通過上面的例子我們可以看到通過`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其實每一個定義的{{define}}都是一個獨立的模板,他們相互獨立,是并行存在的關系,內部其實存儲的是類似map的一種關系(key是模板的名稱,value是模板的內容),然后我們通過`ExecuteTemplate`來執行相應的子模板內容,我們可以看到header、footer都是相對獨立的,都能輸出內容,content 中因為嵌套了header和footer的內容,就會同時輸出三個的內容。但是當我們執行`s1.Execute`,沒有任何的輸出,因為在默認的情況下沒有默認的子模板,所以不會輸出任何的東西。
> 同一個集合類的模板是互相知曉的,如果同一模板被多個集合使用,則它需要在多個集合中分別解析
## [](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.4.md#總結)總結
通過上面對模板的詳細介紹,我們了解了如何把動態數據與模板融合:如何輸出循環數據、如何自定義函數、如何嵌套模板等等。通過模板技術的應用,我們可以完成MVC模式中V的處理,接下來的章節我們將介紹如何來處理M和C。
- 第一章 Go環境配置
- 1.1 Go安裝
- 1.2 GOPATH 與工作空間
- 1.3 Go 命令
- 1.4 Go開發工具
- 1.5 小結
- 第二章 Go語言基礎
- 2.1 你好,Go
- 2.2 Go基礎
- 2.3 流程和函數
- 2.4 struct類型
- 2.5 面向對象
- 2.6 interface
- 2.7 并發
- 2.8 總結
- 第三章 Web基礎
- 3.1 Web工作方式
- 3.2 Go搭建一個Web服務器
- 3.3 Go如何使得Web工作
- 3.4 Go的http包詳解
- 3.5 小結
- 第四章 表單
- 4.1 處理表單的輸入
- 4.2 驗證表單的輸入
- 4.3 預防跨站腳本
- 4.4 防止多次遞交表單
- 4.5 處理文件上傳
- 4.6 小結
- 第五章 訪問數據庫
- 5.1 database/sql接口
- 5.2 使用MySQL數據庫
- 5.3 使用SQLite數據庫
- 5.4 使用PostgreSQL數據庫
- 5.5 使用beedb庫進行ORM開發
- 5.6 NOSQL數據庫操作
- 5.7 小結
- 第六章 session和數據存儲
- 6.1 session和cookie
- 6.2 Go如何使用session
- 6.3 session存儲
- 6.4 預防session劫持
- 6.5 小結
- 第七章 文本處理
- 7.1 XML處理
- 7.2 JSON處理
- 7.3 正則處理
- 7.4 模板處理
- 7.5 文件操作
- 7.6 字符串處理
- 7.7 小結
- 第八章 Web服務
- 8.1 Socket編程
- 8.2 WebSocket
- 8.3 REST
- 8.4 RPC
- 8.5 小結
- 第九章 安全與加密
- 9.1 預防CSRF攻擊
- 9.2 確保輸入過濾
- 9.3 避免XSS攻擊
- 9.4 避免SQL注入
- 9.5 存儲密碼
- 9.6 加密和解密數據
- 9.7 小結
- 第十章 國際化和本地化
- 10.1 設置默認地區
- 10.2 本地化資源
- 10.3 國際化站點
- 10.4 小結
- 第十一章 錯誤處理,調試和測試
- 11.1 錯誤處理
- 11.2 使用GDB調試
- 11.3 Go怎么寫測試用例
- 11.4 小結
- 第十二章 部署與維護
- 12.1 應用日志
- 12.2 網站錯誤處理
- 12.3 應用部署
- 12.4 備份和恢復
- 12.5 小結
- 第十三章 如何設計一個Web框架
- 13.1 項目規劃
- 13.2 自定義路由器設計
- 13.3 controller設計
- 13.4 日志和配置設計
- 13.5 實現博客的增刪改
- 13.6 小結
- 第十四章 擴展Web框架
- 14.1 靜態文件支持
- 14.2 Session支持
- 14.3 表單及驗證支持
- 14.4 用戶認證
- 14.5 多語言支持
- 14.6 pprof支持
- 14.7 小結
- 附錄A 參考資料