go template使用
以text/template為例, 而html/template的介面與前者一樣,不再綴述。
模板檔案一般由.tmpl
或.tpl
為字尾。
一些名詞
dot:用表示
.
,相當於一個變數,儲存著傳進來的值,可以改變
pipeline:從字面上看,有點像管道|
,但從文件上看,實際上指的是一切取值操作,包括{{ . }}
、{{ $name }}
,而|
與unix中的一樣:作為函式的最後一個引數
{{ }}:相當於佔位符,主要的邏輯都寫在裡面
1 模板定義
1.1 取值
取值的作用主要是在頁面中表示出來,或者使用一個變數儲存
型別 | 方式 | 解釋 |
---|---|---|
當前值 | {{ . }} |
傳什麼值,就取什麼值,假如直接在頁面上輸出的話,類似fmt.Println |
結構體 | {{ .Field }} |
Field 指的是欄位名,假如結構體巢狀,還可以再使用. 取值,注意:遵循可見性規則 |
變數 | {{ $varName }} |
以$ 開頭,取出變數的值,如何定義且看1.2 |
字典 | {{ .key }} |
取字典key對應的值,不需要首字母大寫,巢狀時,可以再使用. 取值 |
無引數方法 | {{ .Method }} |
執行Method這個方法,第一個返回值作為取出的值,注意:遵循可見性規則,而且返回值有要求,詳細見xxx |
無引數函式 | {{ func }} |
執行func(),把返回值當做結果,詳見xxx |
1.2 變數
有些值,我們可能需要重複使用,最好的方法就是使用一個變數來儲存值減少重複求值的過程。
// 用到的資料
name := "abcdef"
假如我們把name傳進來,那麼假如要求其長度並將其儲存起來,可以使用一個內建函式(見1.4):len
在go template中,用$
表示變數,有點類似shell,使用:
{{ $lenght := len . }}
<h1>長度:{{ $lenght }}</h1>
實際上,還可以這樣寫:
{{ $lenght := . | len }}
<h1>長度:{{ $lenght }}</h1>
利用|
把abcdef
當做最後一個引數傳給len
1.3 動作
go template的動作(action)有點像,django的模板引擎中的tag,不過兩者之間還是有較大的不一樣。
1.3.1 註釋
註釋,執行時會忽略。可以多行。註釋不能巢狀,並且必須緊貼分界符始止,就像這裡表示的一樣。
{{/* 我是註釋啊 */}}
1.3.2 if判斷
有以下3種
{{if pipeline}} T1 {{end}}
如果pipeline的值為empty,不產生輸出,否則輸出T1執行結果。不改變dot的值。
Empty值包括false、0、任意nil指標或者nil介面,任意長度為0的陣列、切片、字典。
如:
<p>{{ if . }}welcome{{ end }}</p>
在這裡我傳的是一個bool值,為true,因此p便籤中的內容為welcome
{{if pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值為empty,輸出T0執行結果,否則輸出T1執行結果。不改變dot的值。
<p>{{ if . }}welcome{{ else }} Get out!{{ end }}</p>
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
用於簡化if-else鏈條,else action可以直接包含另一個if;等價於:
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
<p>{{ if eq . 1 }}count=1{{ else if eq . 2}} count=2{{ else if eq . 3}} count=3{{ end }}</p>
這裡的eq
是一個內建函式,相當於==
。
1.3.3 with
這裡的with
與並不是Python的with。go template的with
相當於可以暫時修改dot的if。
形式一: {{with pipeline}} T1 {{end}}
如果pipeline為empty不產生輸出,否則將dot設為pipeline的值並執行T1。不修改外面的dot。
{{ with gt . 18}} result:{{ . }}, 嘿嘿嘿 {{end}}
這裡的gt相當於>
, 因此假如執行成功,那麼.
必然是true
.
形式二:{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline為empty,不改變dot並執行T0,否則dot設為pipeline的值並執行T1。不修改外面的dot。
實際上這和上面的一樣,就是多了個{{ else }}
{{ with gt . 18}} result:{{ . }}, 嘿嘿嘿 {{else}} 才{{ . }}歲,未成年 {{end}}
1.3.4 遍歷
遍歷的值必須是陣列、切片、字典或者通道。
- 簡單形式:
{{range pipeline}} T1 {{end}}
如果pipeline的值其長度為0,不會有任何輸出;
否則dot依次設為陣列、切片、字典或者通道的每一個成員元素並執行T1;
如果pipeline的值為字典,且鍵可排序的基本型別,元素也會按鍵的順序排序。
如,要遍歷的資料如下:
data := map[string]string{
"張三": "hello",
"李四": "word",
}
在模板檔案中定義:
<div>
{{ range . }}
<p>{{ . }}</p>
{{ end }}
</div>
所得到的結果:
<div>
<p>hello</p>
<p>word</p>
</div>
- 加else形式:
{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其長度為0,不改變dot的值並執行T0;否則會修改dot並執行T1。
假如資料是一個空切片[]int{}
:
<div>
{{ range . }}
<p>{{ . }}</p>
{{ else }}
<span>no data</span>
{{ end }}
</div>
結果是<span>no data</span>
1.3.5 巢狀與繼承
define
當解析模板時,可以定義另一個模板,該模板會和當前解析的模板相關聯。模板必須定義在當前模板的最頂層,就像go程式裡的全域性變數一樣。
這種定義模板的語法是將每一個子模板的宣告放在"define"和"end" action內部。
如:
{{ define "rd"}}
<div>
{{ range . }}
<p>{{ . }}</p>
{{ else }}
<span>v2 no data</span>
{{ end }}
</div>
{{ end }}
注意:結尾{{ end }}
和define
後面的是字串
template
template
就是對define
定義的模板或其他模板檔案的引用。
template的形式
{{template "name"}}
執行名為name的模板,提供給模板的引數為nil,如模板不存在輸出為""{{template "name" pipeline}}
執行名為name的模板,提供給模板的引數為pipeline的值。
如,在當前檔案中引用:
{{ define "rd"}}
<div>
{{ range . }}
<p>{{ . }}</p>
{{ else }}
<span>v2 no data</span>
{{ end }}
</div>
{{ end }}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>template</title>
</head>
<body>
{{/*引用*/}}
{{ template "rd"}}
</body>
</html>
template引用其他檔案注意:
- 千萬注意,要在程式碼中把檔案讀進來。
t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
也可以使用其他函式 - template後面跟的是完整的檔名
在h1.tpl中:{{ template "h2.tpl" }}
{{template "name" pipeline}}
形式
就是把define中或檔案中的.
替換成template傳進去的值,假如不指定的話,使用當前檔案的.
{{ define "say"}}
<h1>say {{ . }}</h1>
{{ end }}
{{ template "say" "hi"}}
結果:<h1>say hi</h1>
block
block是定義模板{{define "name"}} T1 {{end}}
和執行{{template "name" pipeline}}
縮寫,典型的用法是定義一組根模板,然後通過在其中重新定義塊模板進行自定義。
如,在./templates/base.tpl
中,定義:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
{{block "content" . }}{{end}}
</div>
</body>
</html>
而在其他的模板檔案中:
{{template "base.tpl"}}
{{/* 使用 */}}
{{define "content"}}
<!-- 寫入自己的程式碼 -->
<div>Hello world!</div>
{{end}}
同樣要注意,在解析檔案時把多個模板檔案傳進來
1.3.6 去空
{{- . -}}
使用{{-語法去除模板內容左側的所有空白符號, 使用-}}去除模板內容右側的所有空白符號。
1.4 函式
執行模板時,函式從兩個函式字典中查詢:首先是模板函式字典,然後是全域性函式字典。一般不在模板內定義函式,而是使用Funcs方法新增函式到模板裡。
1.4.1 一般函式
- and
函式返回它的第一個empty引數或者最後一個引數;
就是說"and x y"等價於"if x then y else x";所有引數都會執行;
和上面一樣:Empty值包括false、0、任意nil指標或者nil介面,任意長度為0的陣列、切片、字典。下面再重複
如:
{{ and 1 0 }}
{{/* 返回0 */}}
{{ and 1 1 1}}
{{/* 返回1 */}}
- or
返回第一個非empty引數或者最後一個引數;
亦即"or x y"等價於"if x then x else y";所有引數都會執行;
如:
{{ or 1 0 }}
{{/* 返回1 */}}
{{ or 0 2 1}}
{{/* 返回2 */}}
- not
返回它的單個引數的布林值的否定
如:
{{ not 1 }}
{{/* 返回false */}}
{{ not 0 }}
{{/* 返回true */}}
- len
返回它的引數的整數型別長度
如:
{{/* . 為"abcdef" */}}
{{ len . }}
{{/* 返回6 */}}
- index
執行結果為第一個引數以剩下的引數為索引/鍵指向的值;
如"index x 1 2 3"返回x[1][2][3]的值;每個被索引的主體必須是陣列、切片或者字典。
假如資料為:
data := [][]int{
{1, 2, 3, 4, 5,},
{6, 7, 8, 9, 10,},
}
{{ index . 0 1}}
{{/* 結果為2 */
-
print
即fmt.Sprint
S系列函式會把傳入的資料生成並返回一個字串。以下兩個相同。 -
printf
即fmt.Sprintf -
println
即fmt.Sprintln -
html
返回與其引數的文字表示形式等效的轉義HTML。
這個函式在html/template
中不可用。 -
urlquery
以適合嵌入到網址查詢中的形式返回其引數的文字表示的轉義值。
這個函式在html/template
中不可用。 -
js
返回與其引數的文字表示形式等效的轉義JavaScript。 -
call
執行結果是呼叫第一個引數的返回值,該引數必須是函式型別,其餘引數作為呼叫該函式的引數;
如{{ call .X.Y 1 2 }}
等價於go語言裡的dot.X.Y(1, 2)
;
其中Y是函式型別的欄位或者字典的值,或者其他類似情況;
call的第一個引數的執行結果必須是函式型別的值(和預定義函式如print明顯不同);
該函式型別值必須有1到2個返回值,如果有2個則後一個必須是error介面型別;
如果有2個返回值的方法返回的error非nil,模板執行會中斷並返回給呼叫模板執行者該錯誤;
1.4.2 布林函式
布林函式會將任何型別的零值視為假,其餘視為真。
函式 | 說明 |
---|---|
eq | 如果arg1 == arg2則返回真 |
ne | 如果arg1 != arg2則返回真 |
lt | 如果arg1 < arg2則返回真 |
le | 如果arg1 <= arg2則返回真 |
gt | 如果arg1 > arg2則返回真 |
ge | 如果arg1 >= arg2則返回真 |
注意:
為了簡化多引數相等檢測,eq(只有eq)可以接受2個或更多個引數,它會將第一個引數和其餘引數依次比較,返回下式的結果:
arg1 == arg2 || arg1 ==arg3 || arg1==arg4 ...
(和go的||不一樣,不做惰性運算,所有引數都會執行)
比較函式只適用於基本型別(或重定義的基本型別,如"type Celsius float32")。它們實現了go語言規則的值的比較,但具體的型別和大小會忽略掉,因此任意型別有符號整數值都可以互相比較;任意型別無符號整數值都可以互相比較;等等。但是,整數和浮點數不能互相比較。
1.4.3 自定義函式
使用Funcs
方法,可以將自定義好的函式放入到模板中。
Funcs
的簽名:
func (t *Template) Funcs(funcMap FuncMap) *Template
Funcs方法向模板t的函式字典里加入引數funcMap內的鍵值對。
如果funcMap某個鍵值對的值不是函式型別或者返回值不符合要求會panic。
但是,可以對t函式列表的成員進行重寫。方法返回t以便進行鏈式呼叫。
例子:
例子中的使用的一些方法,見第2部分
// 1. 定義函式,首字母可以小寫,注意返回值
func SayHi(char string) (string, error) {
return "Hi" + char, nil
}
func indexFunc(w http.ResponseWriter, r *http.Request) {
// 2. new
t := template.New("hello.tpl")
// 3. 加入t的函式列表,需要替換掉t
t = t.Funcs(template.FuncMap{"sayHi": SayHi})
// 4. Parse 可以是檔案也可以是字串
t, _ = t.ParseFiles("./hello.tpl")
userName := "xxx"
// 5. 渲染
_ = t.Execute(w, userName)
}
上面程式碼的2-4步,可以使用一段鏈式呼叫完成:
t, _ := template.New("hello.tpl").Funcs(template.FuncMap{"sayHi": SayHi}).ParseFiles("./hello.tpl")
注意事項
template.New
的檔名應該和要渲染的檔名一樣
自定義函式有1-2個返回值,第一個值當做正式返回值。假如有第二個返回值:用來panic
,其型別必須是error
,當對應的值非nil
時,panic
2 一些常用的方法
模板引擎的使用,一般有如下三步:
- 定義模板檔案
- 解析模板檔案
- 模板渲染
其中,第2、3步都要用到一些template的方法(這裡用的是text/template)
2.1 解析模板檔案的方法
// 解析字串
func (t *Template) Parse(src string) (*Template, error)
// 解析1個或多個檔案
func ParseFiles(filenames ...string) (*Template, error)
// 解析用正則匹配到的檔案
func ParseGlob(pattern string) (*Template, error)
使用
1. Parse
這裡使用New
函式:
func New(name string) *Template
其作用是建立一個名為name的模板。
t, _ := template.New("test.tpl").Parse("<h1>{{ . }}</h1>")
Parse可以多次呼叫,但只有第一次呼叫可以包含空格、註釋和模板定義之外的文字。
如果後面的呼叫在解析後仍剩餘文字會引發錯誤、返回nil且丟棄剩餘文字;
如果解析得到的模板已有相關聯的同名模板,會覆蓋掉原模板。
2. ParseFiles
t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl", "./h3.tpl")
解析匹配引數中的檔案裡的模板定義並將解析結果與t關聯。
如果發生錯誤,會停止解析並返回nil,否則返回(t, nil)。至少要存在一個匹配的檔案。
3. ParseGlob
t, _ := template.ParseGlob("./*.tpl")
解析當前目錄下,所有以.tpl
結尾的檔案,假如有專門的資料夾存放模板檔案,可以使用templates/*.tmpl
(1層目錄時)和templates/**/*.tmpl
(2層目錄時)
匹配時,和ParseFiles
一樣。
2.2 模板渲染的方法
func (t *Template) Execute(wr io.Writer, data interface{}) error
// Execute方法將解析好的模板應用到data上,並將輸出寫入wr。
// 如果執行時出現錯誤,會停止執行,但有可能已經寫入wr部分資料。
// 模板可以安全的併發執行。
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
// 類似Execute,但是使用名為name的t關聯的模板產生輸出。
Execute
渲染的是ParseFiles
或ParseGlob
得到的第一個檔案,假如要讀取多個檔案時,就有可能渲染的不是想要的檔案,所以需要使用ExecuteTemplate
指定一個已經解析的檔案。
如:
t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
userName := "xxx"
_ = t.Execute(w, userName)
怎麼樣都是渲染h1.tpl
,假如要渲染h2.tpl
:
t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
userName := "xxx"
_ = t.ExecuteTemplate(w, "h2.tpl", userName)
注意
ExecuteTemplate
的name可以是define
的模組名
如:
t, _ := template.New("test").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
_ = t.ExecuteTemplate(out, "T", "word")
當然,用其他解析方法也可以。
3 html/template的不同之處
由於html/template
的API和text/template
的API是一樣的,解析和渲染沒有什麼不一樣,但是在定義模板時,考慮到網站的安全性,會對一些風險內容進行轉義,因此會有text/template
有點差別。
如:
t, _ := template.New("test").Parse("{{ . }}")
char := "<script>alert('you have been pwned')</script>!"
_ = t.Execute(w, char)
得到的結果是:<script>alert('you have been pwned')</script>!
與預期不符,為此,html/template
有一個函式可以專門處理這些我們認為安全的字串:template.HTML
。
再使用時,我們可以自定義一個safe
函式,和其他模板引擎一樣,不對一些字串轉義。
func safe(s string) template.HTML {
return template.HTML(s)
}
然後使用:
t, _ := template.New("test").Funcs(template.FuncMap{"safe": safe}).Parse("{{ . | safe }}")
_ = t.Execute(w, char)
補充
如果{{.}
}是非字串型別的值,可以用於JavaScript上下文環境裡:
struct{A,B string}{ "foo", "bar" }
將該值應用在在轉義後的模板裡:
<script>var pair = {{.}};</script>
模板輸出為:
<script>var pair = {"A": "foo", "B": "bar"};</script>
參考:
- https://studygolang.com/static/pkgdoc/pkg/text_template.htm
- https://www.liwenzhou.com/posts/Go/go_template/