## 4.6. 文本和HTML模闆

前面的例子，隻是最簡單的格式，使用Printf是完全足夠的。但是有時候會需要複雜的打印格式，這時候一般需要將格式化代碼分離出來以便更安全地脩改。這寫功能是由text/template和html/template等模闆包提供的，它們提供了一個用變量值填充到一個文本或HTML格式的模闆的機製。

一個模闆是一個字符串或一個文件，里面包含了一個或多個由雙花括號包含的action對象。大部分的字符串隻是按面值打印，但是對於actions部分將觸發其它的行爲。買個actions包好了一個用模闆語言書寫的表達式，一個雖然簡短但是可以輸出複雜的打印值，模闆語言包含通過選擇結構體的成員、調用函數或方法、表達式控製流if-else語句和range循環語句，還有其它實例化模闆等諸多特性。下面是一個簡單的模闆字符串：

{% raw %}

```Go
gopl.io/ch4/issuesreport

const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`
```

{% endraw %}

這個模闆先打印匹配到的issue總數，然後打印每個issue的編號、創建用戶、標題還有存在的時間。每一個action，都有一個當前值的概念，對應點操作符，寫作“.”。當前值“.”最初被初始化爲調用模闆是的參數，在當前例子中對應github.IssuesSearchResult類型的變量。模闆中`{{.TotalCount}}`對應action將展開爲結構體中TotalCount成員以默認的方式打印的值。模闆中`{{range .Items}}`和`{{end}}`對應一個循環action，因此它們直接的內容可能會被展開多次，循環每次迭代的當前值對應當前的Items元素的值。

在一個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（如果不是則發出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 %}

```Go
gopl.io/ch4/issueshtml

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}}</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
$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
```

圖4.4顯示了在web瀏覽器中的效果圖。每個issue包含到Github對應頁面的鏈接。

![](../images/ch4-04.png)

圖4.4中的沒有問題會對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字符串類型。

![](../images/ch4-05.png)

{% raw %}

```Go
gopl.io/ch4/autoescape

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沒有。

![](../images/ch4-06.png)

我們這里隻講述了模闆繫統中最基本的特性。一如旣往，如果想了解更多的信息，請自己査看包文檔：

```
$ go doc text/template
$ go doc html/template
```

**練習 4.14：** 創建一個web服務器，査詢一次GitHub，然後生成BUG報告、里程碑和對應的用戶信息。


