golang 模板 html/template與text/template
html模板生成:
- html/template包實現了資料驅動的模板,用於生成可對抗程式碼注入的安全HTML輸出。它提供了和text/template包相同的介面,Go語言中輸出HTML的場景都應使用text/template包。
模板語法
{{.}}
-
模板語法都包含在{{和}}中間,其中{{.}}中的點表示當前物件。
-
當我們傳入一個結構體物件時,我們可以根據.來訪問結構體的對應欄位。例如:
// 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 } var user = struct { name string age int }{ name:"zhaohaiyu", age:18, } // 利用給定資料渲染模板,並將結果寫入w tmpl.Execute(w, user) } func main() { http.HandleFunc("/", sayHello) err := http.ListenAndServe(":9090", nil) if err != nil { fmt.Println("HTTP server failed,err:", err) return } }
Hello
姓名 {{.Name}}
年齡:{{.Age}}
同理,當我們傳入的變數是map時,也可以在模板檔案中通過.根據key來取值。
註釋
{{/* a comment */}}
pipeline
- pipeline是指產生資料的操作。比如{{.}}、{{.Name}}等。Go的模板語法中支援使用管道符號|連結多個命令,用法和unix下的管道類似:|前面的命令會將運算結果(或返回值)傳遞給後一個命令的最後一個位置。
注意:並不是只有使用了|才是pipeline。Go的模板語法中,pipeline的概念是傳遞資料,只要能產生資料的,都是pipeline。
變數
- 我們還可以在模板中宣告變數,用來儲存傳入模板的資料或其他語句生成的結果。具體語法如下:
$obj := {{.}}
其中$obj是變數的名字,在後續的程式碼中就可以使用該變量了。
移除空格
- 有時候我們在使用模板語法的時候會不可避免的引入一下空格或者換行符,這樣模板最終渲染出來的內容可能就和我們想的不一樣,這個時候可以使用{{-語法去除模板內容左側的所有空白符號, 使用-}}去除模板內容右側的所有空白符號。
例如:{{- .Name -}}
注意:-要緊挨{{和}},同時與模板值之間需要使用空格分隔。
條件判斷
- 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}}
{{range pipeline}} T1 {{else}} T0 {{end}}
with
{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}
預定義函式
執行模板時,函式從兩個函式字典中查詢:首先是模板函式字典,然後是全域性函式字典。一般不在模板內定義函式,而是使用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。
- 這個函式在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,模板執行會中斷並返回給呼叫模板執行者該錯誤;
比較函式
- 布林函式會將任何型別的零值視為假,其餘視為真。
下面是定義為函式的二元比較運算的集合:
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.tmpl")
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.tmpl中按照如下方式使用我們自定義的kua函數了。
{{kua .Name}}
巢狀template
我們可以在template中巢狀其他的template。這個template可以是單獨的檔案,也可以是通過define定義的template。
舉個例子:t.tmpl檔案內容如下:tmpl test
測試巢狀template語法 {{template "ul.tmpl"}}
{{template "ol.tmpl"}}
{{ define "ol.tmpl"}}
吃飯
睡覺
打豆豆
{{end}}
ul.tmpl檔案內容如下:
註釋
日誌
測試
我們註冊一個templDemo路由處理函式.
http.HandleFunc("/tmpl", tmplDemo)
tmplDemo函式的具體內容如下:
func tmplDemo(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
user := UserInfo{
Name: "小王子",
Gender: "男",
Age: 18,
}
tmpl.Execute(w, user)
}
注意:在解析模板時,被巢狀的模板一定要在後面解析,例如上面的示例中t.tmpl模板中嵌套了ul.tmpl,所以ul.tmpl要在t.tmpl後進行解析。
block
{{block "name" pipeline}} T1 {{end}}
block是定義模板{{define "name"}} T1 {{end}}和執行{{template "name" pipeline}}縮寫,典型的用法是定義一組根模板,然後通過在其中重新定義塊模板進行自定義。
定義一個根模板templates/base.tmpl,內容如下:Go Templates
{{block "content" . }}{{end}}
然後定義一個templates/index.tmpl,”繼承”base.tmpl:
{{template "base.tmpl"}}
{{define "content"}}
Hello world!
{{end}}
然後使用template.ParseGlob按照正則匹配規則解析模板檔案,然後通過ExecuteTemplate渲染指定的模板:
func index(w http.ResponseWriter, r *http.Request){
tmpl, err := template.ParseGlob("templates/*.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
err = tmpl.ExecuteTemplate(w, "index.tmpl", nil)
if err != nil {
fmt.Println("render template failed, err:", err)
return
}
}
如果我們的模板名稱衝突了,例如不同業務線下都定義了一個index.tmpl模板,我們可以通過下面兩種方法來解決。
- 在模板檔案開頭使用{{define 模板名}}語句顯式的為模板命名。
- 可以把模板檔案存放在templates資料夾下面的不同目錄中,然後使用template.ParseGlob("templates/**/*.tmpl")解析模板。
修改預設的識別符號
Go標準庫的模板引擎使用的花括號{{和}}作為標識,而許多前端框架(如Vue和AngularJS)也使用{{和}}作為識別符號,所以當我們同時使用Go語言模板引擎和以上前端框架時就會出現衝突,這個時候我們需要修改識別符號,修改前端的或者修改Go語言的。這裡演示如何修改Go語言模板引擎預設的識別符號:
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")
text/template與html/tempalte的區別
html/template針對的是需要返回HTML內容的場景,在模板渲染過程中會對一些有風險的內容進行轉義,以此來防範跨站指令碼攻擊。
例如,我定義下面的模板檔案:
Hello
{{.}}
這個時候傳入一段JS程式碼並使用html/template去渲染該檔案,會在頁面上顯示出轉義後的JS內容。alert('嘿嘿嘿')這就是html/template為我們做的事。
但是在某些場景下,我們如果相信使用者輸入的內容,不想轉義的話,可以自行編寫一個safe函式,手動返回一個template.HTML型別的內容。示例如下:
func xss(w http.ResponseWriter, r *http.Request){
tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{
"safe": func(s string)template.HTML {
return template.HTML(s)
},
}).ParseFiles("./xss.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
jsStr := `alert('嘿嘿嘿')`
err = tmpl.Execute(w, jsStr)
if err != nil {
fmt.Println(err)
}
}
這樣我們只需要在模板檔案不需要轉義的內容後面使用我們定義好的safe函式就可以了。{{ . | safe }}
程式碼生成
生成程式碼用text/template包,模板語法和html/template一樣
package main
import (
"fmt"
"html/template"
"os"
)
var data = `
package main
import "fmt"
func main() {
str := "{{ . }}"
for _, v := range str {
fmt.Println(v)
}
}
`
func main() {
t := template.New("main")
t, err := t.Parse(data)
if err != nil {
fmt.Println("解析失敗")
return
}
var str = "zhaohaiyu"
file, err := os.OpenFile("./print/main.go", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
fmt.Printf("open file:%s failed, err:%v\n", "./print/main.go", err)
return
}
err = t.Execute(file, str)
if err != nil {
fmt.Println("execute failed err:",err)
return
}
return
}
生成的程式碼:
package main
import "fmt"
func main() {
str := "zhaohaiyu"
for _, v := range str {
fmt.Println(v)
}
}
參考文章: