Go標準庫:Go template用法詳解

本文只介紹template的語法和用法,關於template包的函數、方法、template的結構和原理,見:深刻剖析Go templatecss

入門示例

如下爲test.html文件的內容,裏面使用了一個template語法{{.}}html

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web</title>
    </head>
    <body>
        {{ . }}
    </body>
</html>

如下是test.html同目錄下的一個go web程序:java

package main

import (
    "html/template"
    "net/http"
)

func tmpl(w http.ResponseWriter, r *http.Request) {
    t1, err := template.ParseFiles("test.html")
    if err != nil {
        panic(err)
    }
    t1.Execute(w, "hello world")
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/tmpl", tmpl)
    server.ListenAndServe()
}

前面的html文件中使用了一個template的語法{{.}},這部分是須要經過go的template引擎進行解析,而後替換成對應的內容。python

在go程序中,handler函數中使用template.ParseFiles("test.html"),它會自動建立一個模板(關聯到變量t1上),並解析一個或多個文本文件(不只僅是html文件),解析以後就可使用Execute(w,"hello world")去執行解析後的模板對象,執行過程是合併、替換的過程。例如上面的{{.}}中的.會替換成當前對象"hello world",並和其它純字符串內容進行合併,最後寫入w中,也就是發送到瀏覽器"hello world"。c++

本文不解釋這些template包的函數、方法以及更底層的理論知識,本文只解釋template的語法,若是以爲這些沒法理解,或者看不懂官方手冊,請看深刻剖析Go templateweb

關於點"."和做用域

在寫template的時候,會常常用到"."。好比{{.}}{{len .}}{{.Name}}{{$x.Name}}等等。數組

在template中,點"."表明當前做用域的當前對象。它相似於java/c++的this關鍵字,相似於perl/python的self。若是瞭解perl,它更能夠簡單地理解爲默認變量$_瀏覽器

例如,前面示例test.html中{{.}},這個點是頂級做用域範圍內的,它表明Execute(w,"hello worold")的第二個參數"hello world"。也就是說它表明這個字符串對象。安全

再例如,有一個Person struct。curl

type Person struct {
    Name string
    Age  int
}

func main(){
    p := Person{"longshuai",23}
    tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
    _ = tmpl.Execute(os.Stdout, p)
}

這裏{{.Name}}{{.Age}}中的點"."表明的是頂級做用域的對象p,因此Execute()方法執行的時候,會將{{.Name}}替換成p.Name,同理{{.Age}}替換成{{p.Age}}

可是並不是只有一個頂級做用域,range、with、if等內置action都有本身的本地做用域。它們的用法後文解釋,這裏僅引入它們的做用域來解釋"."。

例以下面的例子,若是看不懂也不要緊,只要從中理解"."便可。

package main

import (
    "os"
    "text/template"
)

type Friend struct {
    Fname string
}
type Person struct {
    UserName string
    Emails   []string
    Friends  []*Friend
}

func main() {
    f1 := Friend{Fname: "xiaofang"}
    f2 := Friend{Fname: "wugui"}
    t := template.New("test")
    t = template.Must(t.Parse(
`hello {{.UserName}}!
{{ range .Emails }}
an email {{ . }}
{{- end }}
{{ with .Friends }}
{{- range . }}
my friend name is {{.Fname}}
{{- end }}
{{ end }}`))
    p := Person{UserName: "longshuai",
        Emails:  []string{"a1@qq.com", "a2@gmail.com"},
        Friends: []*Friend{&f1, &f2}}
    t.Execute(os.Stdout, p)
}

輸出結果:

hello longshuai!

an email a1@qq.com
an email a2@gmail.com

my friend name is xiaofang
my friend name is wugui

這裏定義了一個Person結構,它有兩個slice結構的字段。在Parse()方法中:

  • 頂級做用域的{{.UserName}}{{.Emails}}{{.Friends}}中的點都表明Execute()的第二個參數,也就是Person對象p,它們在執行的時候會分別被替換成p.UserName、p.Emails、p.Friends。
  • 由於Emails和Friend字段都是可迭代的,在{{range .Emails}}...{{end}}這一段結構內部an email {{.}},這個"."表明的是range迭代時的每一個元素對象,也就是p.Emails這個slice中的每一個元素。
  • 同理,with結構內部{{range .}}的"."表明的是p.Friends,也就是各個,再此range中又有一層迭代,此內層{{.Fname}}的點表明Friend結構的實例,分別是&f1&f2,因此{{.Fname}}表明實例對象的Fname字段。

去除空白

template引擎在進行替換的時候,是徹底按照文本格式進行替換的。除了須要評估和替換的地方,全部的行分隔符、空格等等空白都原樣保留。因此,對於要解析的內容,不要隨意縮進、隨意換行

能夠在{{符號的後面加上短橫線並保留一個或多個空格"- "來去除它前面的空白(包括換行符、製表符、空格等),即{{- xxxx

}}的前面加上一個或多個空格以及一個短橫線"-"來去除它後面的空白,即xxxx -}}

例如:

{{23}} < {{45}}        -> 23 < 45
{{23}} < {{- 45}}      ->  23 <45
{{23 -}} < {{45}}      ->  23< 45
{{23 -}} < {{- 45}}    ->  23<45

其中{{23 -}}中的短橫線去除了這個替換結構後面的空格,即}} <中間的空白。同理{{- 45}}的短橫線去除了< {{中間的空白。

再看上一節的例子中:

t.Parse(
`hello {{.UserName}}!
{{ range .Emails }}
an email {{ . }}
{{- end }}
{{ with .Friends }}
{{- range . }}
my friend name is {{.Fname}}
{{- end }}
{{ end }}`)

注意,上面沒有進行縮進。由於縮進的製表符或空格在替換的時候會保留。

第一行和第二行之間輸出時會換行輸出,不只如此,range {{.Emails}}自身也佔一行,在替換的時候它會被保留爲空行。除非range前面沒加{{-。因爲range的{{- end加上了去除前綴空白,因此每次迭代的時候,每一個元素之間都換行輸出但卻很少一空行,若是這裏的end去掉{{-,則每一個迭代的元素之間輸出的時候都會有空行。同理後面的with和range。

註釋

註釋方式:{{/* a comment */}}

註釋後的內容不會被引擎進行替換。但須要注意,註釋行在替換的時候也會佔用行,因此應該去除前綴和後綴空白,不然會多一空行。

{{- /* a comment without prefix/suffix space */}}
{{/* a comment without prefix/suffix space */ -}}
{{- /* a comment without prefix/suffix space */ -}}

注意,應該只去除前綴或後綴空白,不要同時都去除,不然會破壞原有的格式。例如:

t.Parse(
`hello {{.UserName}}!
{{- /* this line is a comment */}}
{{ range .Emails }}
an email {{ . }}
{{- end }}

管道pipeline

pipeline是指產生數據的操做。好比{{.}}{{.Name}}funcname args等。

可使用管道符號|連接多個命令,用法和unix下的管道相似:|前面的命令將運算結果(或返回值)傳遞給後一個命令的最後一個位置。

例如:

{{.}} | printf "%s\n" "abcd"

{{.}}的結果將傳遞給printf,且傳遞的參數位置是"abcd"以後。

命令能夠有超過1個的返回值,這時第二個返回值必須爲err類型。

須要注意的是,並不是只有使用了|纔是pipeline。Go template中,pipeline的概念是傳遞數據,只要能產生數據的,都是pipeline。這使得某些操做能夠做爲另外一些操做內部的表達式先運行獲得結果,就像是Unix下的命令替換同樣。

例如,下面的(len "output")是pipeline,它總體先運行。

{{println (len "output")}}

下面是Pipeline的幾種示例,它們都輸出"output"

{{`"output"`}}
{{printf "%q" "output"}}
{{"output" | printf "%q"}}
{{printf "%q" (print "out" "put")}}
{{"put" | printf "%s%s" "out" | printf "%q"}}
{{"output" | printf "%s" | printf "%q"}}

變量

能夠在template中定義變量:

// 未定義過的變量
$var := pipeline

// 已定義過的變量
$var = pipeline

例如:

{{- $how_long :=(len "output")}}
{{- println $how_long}}   // 輸出6

再例如:

tx := template.Must(template.New("hh").Parse(
`{{range $x := . -}}
{{$y := 333}}
{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
{{- end}}
`))
s := []int{11, 22, 33, 44, 55}
_ = tx.Execute(os.Stdout, s)

輸出結果:

44 333 444
55 333 444

上面的示例中,使用range迭代slice,每一個元素都被賦值給變量$x,每次迭代過程當中,都新設置一個變量$y,在內層嵌套的if結構中,可使用這個兩個外層的變量。在if的條件表達式中,使用了一個內置的比較函數gt,若是$x大於33,則爲true。在println的參數中還定義了一個$z,之因此能定義,是由於($z := 444)的過程是一個Pipeline,能夠先運行。

須要注意三點:

  1. 變量有做用域,只要出現end,則當前層次的做用域結束。內層能夠訪問外層變量,但外層不能訪問內層變量
  2. 有一個特殊變量$,它表明模板的最頂級做用域對象(通俗地理解,是以模板爲全局做用域的全局變量),在Execute()執行的時候進行賦值,且一直不變。例如上面的示例中,$ = [11 22 33 44 55]。再例如,define定義了一個模板t1,則t1中的$做用域只屬於這個t1。
  3. 變量不可在模板之間繼承。普通變量可能比較容易理解,但對於特殊變量"."和"$",比較容易搞混。見下面的例子。

例如:

func main() {
    t1 := template.New("test1")
    tmpl, _ := t1.Parse(
`
{{- define "T1"}}ONE {{println .}}{{end}}
{{- define "T2"}}{{template "T1" $}}{{end}}
{{- template "T2" . -}}
`)
    _ = tmpl.Execute(os.Stdout, "hello world")
}

上面使用define額外定義了T1和T2兩個模板,T2中嵌套了T1。{{template "T2" .}}的點表明頂級做用域的"hello world"對象。在T2中使用了特殊變量$,這個$的範圍是T2的,不會繼承頂級做用域"hello world"。但由於執行T2的時候,傳遞的是".",因此這裏的$的值仍然是"hello world"。

不只$不會在模板之間繼承,.也不會在模板之間繼承(其它全部變量都不會繼承)。實際上,template能夠看做是一個函數,它的執行過程是template("T2",.)。若是把上面的$換成".",結果是同樣的。若是換成{{template "T2"}},則$=nil

若是看不懂這些,後文有解釋。

條件判斷

有如下幾種if條件判斷語句,其中第三和第四是等價的。

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

須要注意的是,pipeline爲false的狀況是各類數據對象的0值:數值0,指針或接口是nil,數組、slice、map或string則是len爲0。

range...end迭代

有兩種迭表明達式類型:

{{range pipeline}} T1 {{end}}
{{range pipeline}} T1 {{else}} T0 {{end}}

range能夠迭代slice、數組、map或channel。迭代的時候,會設置"."爲當前正在迭代的元素。

對於第一個表達式,當迭代對象的值爲0值時,則range直接跳過,就像if同樣。對於第二個表達式,則在迭代到0值時執行else語句。

tx := template.Must(template.New("hh").Parse(
`{{range $x := . -}}
{{println $x}}
{{- end}}
`))
s := []int{11, 22, 33, 44, 55}
_ = tx.Execute(os.Stdout, s)

需注意的是,range的參數部分是pipeline,因此在迭代的過程當中是能夠進行賦值的。但有兩種賦值狀況:

{{range $value := .}}
{{range $key,$value := .}}

若是range中只賦值給一個變量,則這個變量是當前正在迭代元素的值。若是賦值給兩個變量,則第一個變量是索引值(map/slice是數值,map是key),第二個變量是當前正在迭代元素的值。

下面是在html中使用range的一個示例。test.html文件內容以下:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web</title>
    </head>
    <body>
        <ul>
            {{ range . }}
                <li>{{ . }}</li>
            {{ else }}
                <li> Nothing to show </li>
            {{ end}}
        </ul>
    </body>
</html>

如下是test.html同目錄下的go程序文件:

package main

import (
    "html/template"
    "net/http"
)

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", process)
    server.ListenAndServe()
}

func process(w http.ResponseWriter, r *http.Request) {
    t1 := template.Must(template.ParseFiles("test.html"))
    s := []string{
        "星期一",
        "星期二",
        "星期三",
        "星期四",
        "星期五",
        "星期六",
        "星期日",}
    t1.Execute(w, s)
}

with...end

with用來設置"."的值。兩種格式:

{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}

對於第一種格式,當pipeline不爲0值的時候,點"."設置爲pipeline運算的值,不然跳過。對於第二種格式,當pipeline爲0值時,執行else語句塊,不然"."設置爲pipeline運算的值,並執行T1。

例如:

{{with "xx"}}{{println .}}{{end}}

上面將輸出xx,由於"."已經設置爲"xx"。

內置函數和自定義函數

template定義了一些內置函數,也支持自定義函數。關於如何自定義函數,見深刻剖析Go template

如下是內置的函數列表:

and
    返回第一個爲空的參數或最後一個參數。能夠有任意多個參數。
    and x y等價於if x then y else x

not
    布爾取反。只能一個參數。

or
    返回第一個不爲空的參數或最後一個參數。能夠有任意多個參數。
    "or x y"等價於"if x then x else y"。

print
printf
println
    分別等價於fmt包中的Sprint、Sprintf、Sprintln

len
    返回參數的length。

index
    對可索引對象進行索引取值。第一個參數是索引對象,後面的參數是索引位。
    "index x 1 2 3"表明的是x[1][2][3]。
    可索引對象包括map、slice、array。

call
    顯式調用函數。第一個參數必須是函數類型,且不是template中的函數,而是外部函數。
    例如一個struct中的某個字段是func類型的。
    "call .X.Y 1 2"表示調用dot.X.Y(1, 2),Y必須是func類型,函數參數是1和2。
    函數必須只能有一個或2個返回值,若是有第二個返回值,則必須爲error類型。

除此以外,還內置一些用於比較的函數:

eq arg1 arg2:
    arg1 == arg2時爲true
ne arg1 arg2:
    arg1 != arg2時爲true
lt arg1 arg2:
    arg1 < arg2時爲true
le arg1 arg2:
    arg1 <= arg2時爲true
gt arg1 arg2:
    arg1 > arg2時爲true
ge arg1 arg2:
    arg1 >= arg2時爲true

對於eq函數,支持多個參數:

eq arg1 arg2 arg3 arg4...

它們都和第一個參數arg1進行比較。它等價於:

arg1==arg2 || arg1==arg3 || arg1==arg4

示例:

{{ if (gt $x 33) }}{{println $x}}{{ end }}

嵌套template:define和template

define能夠直接在待解析內容中定義一個模板,這個模板會加入到common結構組中,並關聯到關聯名稱上。若是不理解,仍是建議閱讀深刻剖析Go template

定義了模板以後,可使用template這個action來執行模板。template有兩種格式:

{{template "name"}}
{{template "name" pipeline}}

第一種是直接執行名爲name的template,點設置爲nil。第二種是點"."設置爲pipeline的值,並執行名爲name的template。能夠將template看做是函數:

template("name)
template("name",pipeline)

例如:

func main() {
    t1 := template.New("test1")
    tmpl, _ := t1.Parse(
`{{- define "T1"}}ONE {{println .}}{{end}}
{{- define "T2"}}TWO {{println .}}{{end}}
{{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}}
{{- template "T3" -}}
`)
    _ = tmpl.Execute(os.Stdout, "hello world")
}

輸出結果:

ONE <nil>
TWO haha

上面定義了4個模板,一個是test1,另外三個是使用define來定義的T一、T二、T3,其中t1是test1模板的關聯名稱。T一、T二、T3和test1共享一個common結構。其中T3中包含了執行T1和T2的語句。最後只要{{template T3}}就能夠執行T3,執行T3又會執行T1和T2。也就是實現了嵌套。此外,執行{{template "T1"}}時,點設置爲nil,而{{temlate "T2" "haha"}}的點設置爲了"haha"。

注意,模板之間的變量是不會繼承的

下面是html文件中嵌套模板的幾個示例。

t1.html文件內容以下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=9">
    <title>Go Web Programming</title>
</head>

<body>
    <div> This is t1.html before</div>
    <div>This is the value of the dot in t1.html - [{{ . }}]</div>
    <hr />
    {{ template "t2.html" }}
    <hr />
    <div> This is t1.html after</div>
</body>

</html>

由於內部有{{template "t2.html"}},且此處沒有使用define去定義名爲"t2.html"的模板,因此須要加載解析名爲t2.html的文件。t2.html文件內容以下:

<div style="background-color: yellow;">
    This is t2.html<br/>
    This is the value of the dot in t2.html - [{{ . }}]
</div>

處理這兩個文件的handler函數以下:

func process(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("t1.html", "t2.html")
    t.Execute(w, "Hello World!")
}

上面也能夠不額外定義t2.html文件,而是直接在t1.html文件中使用define定義一個模板。修改t1.html文件以下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=9">
    <title>Go Web Programming</title>
</head>

<body>
    <div> This is t1.html before</div>
    <div>This is the value of the dot in t1.html - [{{ . }}]</div>
    <hr />
    {{ template "t2.html" }}
    <hr />
    <div> This is t1.html after</div>
</body>

</html>

{{define "t2.html"}}
<div style="background-color: yellow;">
    This is t2.html<br/>
    This is the value of the dot in t2.html - [{{ . }}]
</div>
{{end}}

而後在handler中,只需解析t1.html一個文件便可。

func process(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("t1.html")
    t.Execute(w, "Hello World!")
}

block塊

{{block "name" pipeline}} T1 {{end}}
    A block is shorthand for defining a template
        {{define "name"}} T1 {{end}}
    and then executing it in place
        {{template "name" pipeline}}
    The typical use is to define a set of root templates that are
    then customized by redefining the block templates within.

根據官方文檔的解釋:block等價於define定義一個名爲name的模板,並在"有須要"的地方執行這個模板,執行時將"."設置爲pipeline的值。

但應該注意,block的第一個動做是執行名爲name的模板,若是不存在,則在此處自動定義這個模板,並執行這個臨時定義的模板。換句話說,block能夠認爲是設置一個默認模板

例如:

{{block "T1" .}} one {{end}}

它首先表示{{template "T1" .}},也就是說先找到T1模板,若是T1存在,則執行找到的T1,若是沒找到T1,則臨時定義一個{{define "T1"}} one {{end}},並執行它。

下面是正常狀況下不使用block的示例。

home.html文件內容以下:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web Programming</title>
    </head>
    <body>
        {{ template "content" }}
    </body>
</html>

在此文件中指定了要執行一個名爲"content"的模板,但此文件中沒有使用define定義該模板,因此須要在其它文件中定義名爲content的模板。如今分別在兩個文件中定義兩個content模板:

red.html文件內容以下:

{{ define "content" }}
    <h1 style="color: red;">Hello World!</h1>
{{ end }}

blue.html文件內容以下:

{{ define "content" }}
    <h1 style="color: blue;">Hello World!</h1>
{{ end }}

在handler中,除了解析home.html,還根據須要解析red.html或blue.html:

func process(w http.ResponseWriter, r *http.Request) {
    rand.Seed(time.Now().Unix())
    t := template.New("test")
    if rand.Intn(10) > 5 {
        t, _ = template.ParseFiles("home.html", "red.html")
    } else {
        t, _ = template.ParseFiles("home.html", "blue.html")
    }
    t.Execute(w,"")
}

若是使用block,那麼能夠設置默認的content模板。例如將本來定義在blue.html中的content設置爲默認模板。

修改home.html:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web Programming</title>
    </head>
    <body>
        {{ block "content" . }}
            <h1 style="color: blue;">Hello World!</h1>
        {{ end }}
    </body>
</html>

而後修改handler:

func process(w http.ResponseWriter, r *http.Request) {
    rand.Seed(time.Now().Unix())
    t := template.New("test")
    if rand.Intn(10) > 5 {
        t, _ = template.ParseFiles("home.html", "red.html")
    } else {
        t, _ = template.ParseFiles("home.html")
    }
    t.Execute(w,"")
}

當執行else語句塊的時候,發現home.html中要執行名爲content的模板,但在ParseFiles()中並無解析包含content模板的文件。因而執行block定義的content模板。而執行非else語句的時候,由於red.html中定義了content,會直接執行red.html中的content。

block一般設置在頂級的根文件中,例如上面的home.html中。

html/template的上下文感知

對於html/template包,有一個很好用的功能:上下文感知。text/template沒有該功能。

上下文感知具體指的是根據所處環境css、js、html、url的path、url的query,自動進行不一樣格式的轉義。

例如,一個handler函數的代碼以下:

func process(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("test.html")
    content := `I asked: <i>"What's up?"</i>`
    t.Execute(w, content)
}

上面content是Execute的第二個參數,它的內容是包含了特殊符號的字符串。

下面是test.html文件的內容:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web Programming</title>
    </head>
    <body>
        <div>{{ . }}</div>
        <div><a href="/{{ . }}">Path</a></div>
        <div><a href="/?q={{ . }}">Query</a></div>
        <div><a onclick="f('{{ . }}')">Onclick</a></div>
    </body>
</html>

上面test.html中有4個不一樣的環境,分別是html環境、url的path環境、url的query環境以及js環境。雖然對象都是{{.}},但解析執行後的值是不同的。若是使用curl獲取源代碼,結果將以下:

<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Go Web Programming</title>
</head>

<body>
    <div>I asked: &lt;i&gt;&#34;What&#39;s up?&#34;&lt;/i&gt;</div>
    <div>
        <a href="/I%20asked:%20%3ci%3e%22What%27s%20up?%22%3c/i%3e">
            Path
        </a>
    </div>
    <div>
        <a href="/?q=I%20asked%3a%20%3ci%3e%22What%27s%20up%3f%22%3c%2fi%3e">
            Query
        </a>
    </div>
    <div>
        <a onclick="f('I asked: \x3ci\x3e\x22What\x27s up?\x22\x3c\/i\x3e')">
            Onclick
        </a>
    </div>
</body>

</html>

不轉義

上下文感知的自動轉義能讓程序更加安全,好比防止XSS攻擊(例如在表單中輸入帶有<script>...</script>的內容並提交,會使得用戶提交的這部分script被執行)。

若是確實不想轉義,能夠進行類型轉換。

type CSS
type HTML
type JS
type URL

轉換成指定個時候,字符都將是字面意義。

例如:

func process(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("tmpl.html")
    t.Execute(w, template.HTML(r.FormValue("comment")))
}
相關文章
相關標籤/搜索