## 4.6. 文本和HTML模板
前面的例子,只是最簡單的格式化,使用Printf是完全足夠的。但是有時候會需要復雜的打印格式,這時候一般需要將格式化代碼分離出來以便更安全地修改。這些功能是由text/template和html/template等模板包提供的,它們提供了一個將變量值填充到一個文本或HTML格式的模板的機制。
一個模板是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的`{{action}}`對象。大部分的字符串只是按字面值打印,但是對于actions部分將觸發其它的行為。每個actions都包含了一個用模板語言書寫的表達式,一個action雖然簡短但是可以輸出復雜的打印值,模板語言包含通過選擇結構體的成員、調用函數或方法、表達式控制流if-else語句和range循環語句,還有其它實例化模板等諸多特性。下面是一個簡單的模板字符串:
{% raw %}
<u><i>gopl.io/ch4/issuesreport</i></u>
```Go
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`
```
{% endraw %}
{% raw %}
這個模板先打印匹配到的issue總數,然后打印每個issue的編號、創建用戶、標題還有存在的時間。對于每一個action,都有一個當前值的概念,對應點操作符,寫作“.”。當前值“.”最初被初始化為調用模板時的參數,在當前例子中對應github.IssuesSearchResult類型的變量。模板中`{{.TotalCount}}`對應action將展開為結構體中TotalCount成員以默認的方式打印的值。模板中`{{range .Items}}`和`{{end}}`對應一個循環action,因此它們之間的內容可能會被展開多次,循環每次迭代的當前值對應當前的Items元素的值。
{% endraw %}
在一個action中,`|`操作符表示將前一個表達式的結果作為后一個函數的輸入,類似于UNIX中管道的概念。在Title這一行的action中,第二個操作是一個printf函數,是一個基于fmt.Sprintf實現的內置函數,所有模板都可以直接使用。對于Age部分,第二個動作是一個叫daysAgo的函數,通過time.Since函數將CreatedAt成員轉換為過去的時間長度:
```Go
func daysAgo(t time.Time) int {
return int(time.Since(t).Hours() / 24)
}
```
需要注意的是CreatedAt的參數類型是time.Time,并不是字符串。以同樣的方式,我們可以通過定義一些方法來控制字符串的格式化(§2.5),一個類型同樣可以定制自己的JSON編碼和解碼行為。time.Time類型對應的JSON值是一個標準時間格式的字符串。
生成模板的輸出需要兩個處理步驟。第一步是要分析模板并轉為內部表示,然后基于指定的輸入執行模板。分析模板部分一般只需要執行一次。下面的代碼創建并分析上面定義的模板templ。注意方法調用鏈的順序:template.New先創建并返回一個模板;Funcs方法將daysAgo等自定義函數注冊到模板中,并返回模板;最后調用Parse函數分析模板。
```Go
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
```
因為模板通常在編譯時就測試好了,如果模板解析失敗將是一個致命的錯誤。template.Must輔助函數可以簡化這個致命錯誤的處理:它接受一個模板和一個error類型的參數,檢測error是否為nil(如果不是nil則發出panic異常),然后返回傳入的模板。我們將在5.9節再討論這個話題。
一旦模板已經創建、注冊了daysAgo函數、并通過分析和檢測,我們就可以使用github.IssuesSearchResult作為輸入源、os.Stdout作為輸出源來執行模板:
```Go
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
```
程序輸出一個純文本報告:
```
$ go build gopl.io/ch4/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
13 issues:
----------------------------------------
Number: 5680
User: eaigner
Title: encoding/json: set key converter on en/decoder
Age: 750 days
----------------------------------------
Number: 6050
User: gopherbot
Title: encoding/json: provide tokenizer
Age: 695 days
----------------------------------------
...
```
現在讓我們轉到html/template模板包。它使用和text/template包相同的API和模板語言,但是增加了一個將字符串自動轉義特性,這可以避免輸入字符串和HTML、JavaScript、CSS或URL語法產生沖突的問題。這個特性還可以避免一些長期存在的安全問題,比如通過生成HTML注入攻擊,通過構造一個含有惡意代碼的問題標題,這些都可能讓模板輸出錯誤的輸出,從而讓他們控制頁面。
下面的模板以HTML格式輸出issue列表。注意import語句的不同:
{% raw %}
<u><i>gopl.io/ch4/issueshtml</i></u>
```Go
import "html/template"
var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>State</th>
<th>User</th>
<th>Title</th>
</tr>
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
<td>{{.State}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))
```
{% endraw %}
下面的命令將在新的模板上執行一個稍微不同的查詢:
```
$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
```
圖4.4顯示了在web瀏覽器中的效果圖。每個issue包含到Github對應頁面的鏈接。

圖4.4中issue沒有包含會對HTML格式產生沖突的特殊字符,但是我們馬上將看到標題中含有`&`和`<`字符的issue。下面的命令選擇了兩個這樣的issue:
```
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
```
圖4.5顯示了該查詢的結果。注意,html/template包已經自動將特殊字符轉義,因此我們依然可以看到正確的字面值。如果我們使用text/template包的話,這2個issue將會產生錯誤,其中“&lt;”四個字符將會被當作小于字符“<”處理,同時“<link>”字符串將會被當作一個鏈接元素處理,它們都會導致HTML文檔結構的改變,從而導致有未知的風險。
我們也可以通過對信任的HTML字符串使用template.HTML類型來抑制這種自動轉義的行為。還有很多采用類型命名的字符串類型分別對應信任的JavaScript、CSS和URL。下面的程序演示了兩個使用不同類型的相同字符串產生的不同結果:A是一個普通字符串,B是一個信任的template.HTML字符串類型。

{% raw %}
<u><i>gopl.io/ch4/autoescape</i></u>
```Go
func main() {
const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
t := template.Must(template.New("escape").Parse(templ))
var data struct {
A string // untrusted plain text
B template.HTML // trusted HTML
}
data.A = "<b>Hello!</b>"
data.B = "<b>Hello!</b>"
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}
```
{% endraw %}
圖4.6顯示了出現在瀏覽器中的模板輸出。我們看到A的黑體標記被轉義失效了,但是B沒有。

我們這里只講述了模板系統中最基本的特性。一如既往,如果想了解更多的信息,請自己查看包文檔:
```
$ go doc text/template
$ go doc html/template
```
**練習 4.14:** 創建一個web服務器,查詢一次GitHub,然后生成BUG報告、里程碑和對應的用戶信息。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言