go template使用

忞翛發表於2020-07-08

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種

  1. {{if pipeline}} T1 {{end}}
    如果pipeline的值為empty,不產生輸出,否則輸出T1執行結果。不改變dot的值。
    Empty值包括false、0、任意nil指標或者nil介面,任意長度為0的陣列、切片、字典。
    如:
<p>{{ if . }}welcome{{ end }}</p>

在這裡我傳的是一個bool值,為true,因此p便籤中的內容為welcome

  1. {{if pipeline}} T1 {{else}} T0 {{end}}
    如果pipeline的值為empty,輸出T0執行結果,否則輸出T1執行結果。不改變dot的值。
<p>{{ if . }}welcome{{ else }}  Get out!{{ end }}</p>
  1. {{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 遍歷

遍歷的值必須是陣列、切片、字典或者通道。

  1. 簡單形式: {{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>
  1. 加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引用其他檔案注意:

  1. 千萬注意,要在程式碼中把檔案讀進來。
    t, _ := template.ParseFiles("./h1.tpl", "./h2.tpl")
    也可以使用其他函式
  2. 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 一些常用的方法

模板引擎的使用,一般有如下三步:

  1. 定義模板檔案
  2. 解析模板檔案
  3. 模板渲染

其中,第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,但是使用名為namet關聯的模板產生輸出。

Execute渲染的是ParseFilesParseGlob得到的第一個檔案,假如要讀取多個檔案時,就有可能渲染的不是想要的檔案,所以需要使用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)

得到的結果是:&lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
與預期不符,為此,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>

參考:

  1. https://studygolang.com/static/pkgdoc/pkg/text_template.htm
  2. https://www.liwenzhou.com/posts/Go/go_template/

我的github
我的部落格
我的筆記

相關文章