html/template包實現了數據驅動的模板,用於生成可對抗代碼注入的安全HTML輸出。它提供了和text/template包相同的接口,Go語言中輸出HTML的場景都應使用text/template包。

模板

在基於MVC的Web架構中,我們通常需要在后端渲染一些數據到HTML文件中,從而實現動態的網頁效果。

模板示例

通過將模板應用於一個數據結構(即該數據結構作為模板的參數)來執行,來獲得輸出。模板中的注釋引用數據接口的元素(一般如結構體的字段或者字典的鍵)來控制執行過程和獲取需要呈現的值。模板執行時會遍歷結構並將指針表示為’.‘(稱之為”dot”)指向運行過程中數據結構的當前位置的值。

用作模板的輸入文本必須是utf-8編碼的文本。”Action”—數據運算和控制單位—由”{{“和”}}“界定;在Action之外的所有文本都不做修改的拷貝到輸出中。Action內部不能有換行,但注釋可以有換行。

HTML文件代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    <p>Hello {{.}}</p>
</body>
</html>

我們的HTTP server端代碼如下:

// main.go

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板對象
    tmpl, err := template.ParseFiles("./hello.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    // 利用給定數據渲染模板,並將結果寫入w
    tmpl.Execute(w, "小明")
}
func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        fmt.Println("HTTP server failed,err:", err)
        return
    }
}

模板語法

{{.}}

模板語法都包含在{{}}中間,其中{{.}}中的點表示當前對象。

當我們傳入一個結構體對象時,我們可以根據.來訪問結構體的對應字段。例如:

// main.go

type UserInfo struct {
    Name   string
    Gender string
    Age    int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板對象
    tmpl, err := template.ParseFiles("./hello.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    // 利用給定數據渲染模板,並將結果寫入w
    user := UserInfo{
        Name:   "小明",
        Gender: "男",
        Age:    18,
    }
    tmpl.Execute(w, user)
}

HTML文件代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    <p>Hello {{.Name}}</p>
    <p>性別:{{.Gender}}</p>
    <p>年齡:{{.Name}}</p>
</body>
</html>

同理,當我們傳入的變量是map時,也可以在模板文件中通過.根據key來取值。

注釋

{{/* a comment */}}
注釋,執行時會忽略。可以多行。注釋不能嵌套,並且必須緊貼分界符始止。

pipeline

pipeline是指產生數據的操作。比如{{.}}{{.Name}}等。Go的模板語法中支持使用管道符號|鏈接多個命令,用法和unix下的管道類似:|前面的命令會將運算結果(或返回值)傳遞給后一個命令的最后一個位置。

注意:並不是只有使用了|才是pipeline。Go的模板語法中,pipeline的概念是傳遞數據,只要能產生數據的,都是pipeline

變量

Action里可以初始化一個變量來捕獲管道的執行結果。初始化語法如下:

$variable := pipeline

其中$variable是變量的名字。聲明變量的action不會產生任何輸出。

條件判斷

Go模板語法中的條件判斷有以下幾種:

{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

range

Go的模板語法中使用range關鍵字進行遍歷,有以下兩種寫法,其中pipeline的值必須是數組、切片、字典或者通道。

{{range pipeline}} T1 {{end}}
如果pipeline的值其長度為0,不會有任何輸出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其長度為0,則會執行T0。

with

{{with pipeline}} T1 {{end}}
如果pipeline為empty不產生輸出,否則將dot設為pipeline的值並執行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline為empty,不改變dot並執行T0,否則dot設為pipeline的值並執行T1。

預定義函數

執行模板時,函數從兩個函數字典中查找:首先是模板函數字典,然后是全局函數字典。一般不在模板內定義函數,而是使用Funcs方法添加函數到模板里。

預定義的全局函數如下:

and
    函數返回它的第一個empty參數或者最后一個參數;
    就是說"and x y"等價於"if x then y else x";所有參數都會執行;
or
    返回第一個非empty參數或者最后一個參數;
    亦即"or x y"等價於"if x then x else y";所有參數都會執行;
not
    返回它的單個參數的布爾值的否定
len
    返回它的參數的整數類型長度
index
    執行結果為第一個參數以剩下的參數為索引/鍵指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每個被索引的主體必須是數組、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回其參數文本表示的HTML逸碼等價表示。
urlquery
    返回其參數文本表示的可嵌入URL查詢的逸碼等價表示。
js
    返回其參數文本表示的JavaScript逸碼等價表示。
call
    執行結果是調用第一個參數的返回值,該參數必須是函數類型,其余參數作為調用該函數的參數;
    如"call .X.Y 1 2"等價於go語言里的dot.X.Y(1, 2);
    其中Y是函數類型的字段或者字典的值,或者其他類似情況;
    call的第一個參數的執行結果必須是函數類型的值(和預定義函數如print明顯不同);
    該函數類型值必須有1到2個返回值,如果有2個則后一個必須是error接口類型;
    如果有2個返回值的方法返回的error非nil,模板執行會中斷並返回給調用模板執行者該錯誤;

比較函數

布爾函數會將任何類型的零值視為假,其余視為真。

下面是定義為函數的二元比較運算的集合:

eq      如果arg1 == arg2則返回真
ne      如果arg1 != arg2則返回真
lt      如果arg1 < arg2則返回真
le      如果arg1 <= arg2則返回真
gt      如果arg1 > arg2則返回真
ge      如果arg1 >= arg2則返回真

為了簡化多參數相等檢測,eq(只有eq)可以接受2個或更多個參數,它會將第一個參數和其余參數依次比較,返回下式的結果:

{{eq arg1 arg2 arg3}}

比較函數只適用於基本類型(或重定義的基本類型,如”type Celsius float32”)。但是,整數和浮點數不能互相比較。

自定義函數

Go的模板支持自定義函數。

func sayHello(w http.ResponseWriter, r *http.Request) {
    htmlByte, err := ioutil.ReadFile("./hello.html")
    if err != nil {
        fmt.Println("read html failed, err:", err)
        return
    }
    // 自定義一個誇人的模板函數
    kua := func(arg string) (string, error) {
        return arg + "真帥", nil
    }
    // 采用鏈式操作在Parse之前調用Funcs添加自定義的kua函數
    tmpl, err := template.New("hello").Funcs(template.FuncMap{"kua": kua}).Parse(string(htmlByte))
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }

    user := UserInfo{
        Name:   "小明",
        Gender: "男",
        Age:    18,
    }
    // 使用user渲染模板,並將結果寫入w
    tmpl.Execute(w, user)
}

我們可以在模板文件hello.html中使用我們自定義的kua函數了。

{{kua .Name}}

嵌套template

我們可以在template中嵌套其他的template。這個template可以是單獨的文件,也可以是通過define定義的template。

舉個例子: t.html文件內容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>
    
    <h1>測試嵌套template語法</h1>
    <hr>
    {{template "ul.html"}}
    <hr>
    {{template "ol.html"}}
</body>
</html>

{{ define "ol.html"}}
<h1>這是ol.html</h1>
<ol>
    <li>吃飯</li>
    <li>睡覺</li>
    <li>打豆豆</li>
</ol>
{{end}}

ul.html文件內容如下:

<ul>
    <li>注釋</li>
    <li>日志</li>
    <li>測試</li>
</ul>

我們注冊一個templDemo路由處理函數.

http.HandleFunc("/tmpl", tmplDemo)

tmplDemo函數的具體內容如下:

func tmplDemo(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("./t.html", "./ul.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    user := UserInfo{
        Name:   "小明",
        Gender: "男",
        Age:    18,
    }
    tmpl.Execute(w, user)
}