Go标準庫提供了幾個package可以産生輸出結果,而text/template 提供了基于模闆輸出文本内容的功能。html/template則是産生 安全的HTML格式的輸出。這兩個包使用相同的接口,但是我下面的例子主要面向HTML應用。
解析和建立模闆
命名模闆
模闆沒有限定擴充名,最流行的字尾是
.tmpl
, vim-go提供了對它的支援,并且godoc的例子中也使用這個字尾。Atom 和 GoSublime 對
.gohtml
字尾的檔案提供了文法高亮的支援。通過對代碼庫的分析統計發現
.tpl
字尾也被經常使用。當然字尾并不重要,在項目中保持清晰和一緻即可。
建立模闆
tpl, err := template.Parse(filename)
得到檔案名為名字的模闆,并儲存在
tpl
變量中。tpl可以被執行來顯示模闆。
解析多個模闆
template.ParseFiles(filenames)
可以解析一組模闆,使用檔案名作為模闆的名字。
template.ParseGlob(pattern)
會根據
pattern
解析所有比對的模闆并儲存。
解析字元串模闆
t, err := template.New("foo").Parse(\
{ {define “T”}}Hello, { {.}}!{ {end}}
)
可以解析字元串模闆,并設定它的名字。
執行模闆
執行簡單模闆
又兩種方式執行模闆。簡單的模闆
tpl
可以通過
tpl.Execute(io.Writer, data)
去執行, 模闆渲染後的内容寫入到
io.Writer
中。
Data
是傳給模闆的動态資料。
執行命名的模闆
tpl.ExecuteTemplate(io.Writer, name, data)
和上面的簡單模闆類似,隻不過傳入了一個模闆的名字,指定要渲染的模闆(因為
tpl
可以包含多個模闆)。
模闆編碼和HTML
上下文編碼
html/template
基于上下文資訊進行編碼,是以任何需要編碼的字元都能被正确的進行編碼。
例如
"<h1>A header!</h1>"
中的尖括号會被編碼為
<h1>A header!</h1>
。
template.HTML
可以告訴Go要處理的字元串是安全的,不需要編碼。
template.HTML("<h1>A Safe header</h1>")
會輸出
<h1>A Safe header</h1>
,注意這個方法處理使用者的輸入的時候比較危險。
html/template
還可以根據模闆中的屬性進行不同的編碼。(The go html/template package is aware of attributes within the template and will encode values differently based on the attribute.)
Go 模闆也可以應用javascript。struct和map被展開為JSON 對象,引号會被增加到字元串中,,用做函數參數和變量的值。
// Go
type Cat struct {
Name string
Age int
}
kitten := Cat{"Sam", 12}
// Template
<script>
var cat = { {.kitten}}
</script>
// Javascript
var cat = {"Name":"Sam", "Age" 12}
安全字元串和 HTML注釋
預設情況下
html/template
會删除模闆中的所有注釋,這會導緻一些問題,因為有些注釋是有用的,比如:
<!--[if IE]>
Place content here to target all Internet Explorer users.
<![endif]-->
我們可以使用自定義的方法建立一個可以傳回注釋的函數。在FuncMap中定義
htmlSafe
方法:
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
}).ParseFiles("hello.gohtml")
這個函數會産生一模一樣的HTML代碼,這個函數可以用在模闆中保留前面的注釋:
{ {htmlSafe "<!--[if IE 6]>" }}
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
{ { htmlSafe "<![endif]-->" }}
模闆變量
. 字元
模闆變量可以是boolean, string, character, integer, floating-point, imaginary 或者 complex constant。傳給模闆這樣的資料就可以通過點号
.
來通路:
{ { . }}
如果資料是複雜類型的資料,可以通過
{ { .FieldName }}
來通路它的字段。
如果字段還是複雜類型,可以鍊式通路
{ { .Struct.StructTwo.Field }}
。
模闆中的變量
傳給模闆的資料可以存在模闆中的變量中,在整個模闆中都能通路。 比如
{ {$number := .}}
, 我們使用
$number
作為變量,儲存傳入的資料,可以使用
{ {$number}}
來通路變量。
{ {$number := .}}
<h1> It is day number { {$number}} of the month </h1>
var tpl *template.Template
tpl = template.Must(template.ParseFiles("templateName"))
err := tpl.ExecuteTemplate(os.Stdout, "templateName", 23)
上面的例子我們把23傳給模闆,模闆的變量
$number
的值是23,可以在模闆中使用。
模闆動作
if/else 語句
像其它語言,模闆支援
if/else
語句。我們可以使用if檢查資料,如果不滿足可以執行else。空值是是false, 0、nil、空字元串或者長度為0的字元串都是false。
<h1>Hello, { {if .Name}} { {.Name}} { {else}} Anonymous { {end}}!</h1>
如果
.Name
存在,會輸出
Hello, Name
,否則輸出
Hello, Anonymous
。
模闆也提供了
{ {else if .Name2 }}
處理多個分支。
移除空格
往模闆中增加不同的值的時候可能會增加一定數量的空格。我們既可以改變我們的模闆以便更好的處理它,忽略/最小化這種效果,或者我們還可以使用減号
-
:
<h1>Hello, { {if .Name}} { {.Name}} { {- else}} Anonymous { {- end}}!</h1>
上面的例子告訴模闆移除
.Name
變量之間的空格。我們在end關鍵字中也加入減号。這樣做的好處是在模闆中我們通過空格更友善程式設計調試,但是生産環境中我們不需要空格。
Range
模闆提供
range
關鍵字來周遊資料。假如我們又下面的資料結構:
type Item struct {
Name string
Price int
}
type ViewData struct {
Name string
Items []Item
}
ViewData
對象傳給模闆,模闆如下:
{ {range .Items}}
<div class="item">
<h3 class="name">{ {.Name}}</h3>
<span class="price">${ {.Price}}</span>
</div>
{ {end}}
對于Items中的每個Item, 我們輸出它的名稱和價格。在range中目前的項目變成了
{ {.}}
,它的屬性是
{ {.Name}}
和
{ {.Price}}
。
模闆函數
模闆包提供了一組預定義的函數,下面介紹一些常用的函數。
擷取索引值
如果傳給模闆的資料是map、slice、數組,那麼我們就可以使用它的索引值。我們使用
{ {index x number}}
來通路
x
的第
number
個元素,
index
是關鍵字。比如
{ {index names 2}}
等價于
names[2]
。
{ {index names 2 3 4}}
等價于
names[2][3][4]
。
<body>
<h1> { {index .FavNums 2 }}</h1>
</body>
type person struct {
Name string
FavNums []int
}
func main() {
tpl := template.Must(template.ParseGlob("*.gohtml"))
tpl.Execute(os.Stdout, &person{"Curtis", []int{7, 11, 94}})
}
上面的例子傳入一個person的資料結構,得到它的FavNums字段中的第三個值。
and 函數
and函數傳回bool值,通過傳回第一個空值或者最後一個值。
and x y
邏輯上相當于
if x then y else x
。考慮下面的代碼:
type User struct {
Admin bool
}
type ViewData struct {
*User
}
傳入一個Admin為true的ViewData對象給模闆:
{ {if and .User .User.Admin}}
You are an admin user!
{ {else}}
Access denied!
{ {end}}
結果會顯示
You are an admin user!
, 如果ViewData不包含一個User值,或者Admin為false,顯示結果則會是
Access denied!
。
or 函數
類似 and 函數,但是隻要遇到 true就傳回。
or x y
等價于
if x then x else y
。 x 非空的情況下y不會被評估。
not 函數
not函數傳回參數的相反值:
{ { if not .Authenticated}}
Access Denied!
{ { end }}
管道
函數調用可以鍊式調用,前一個函數的輸出結果作為下一個函數調用的參數。
html/template
稱之為管道,類似于linux shell指令中的管道一樣,它采用
|
分隔。
注意前一個指令的輸出結果是作為下一個指令的最後一個參數,最終指令的輸出結果就是這個管道的結果。
模闆比較函數
比較
html/template
提供了一系列的函數用做資料的比較。資料的類型隻能是基本類型和命名的基本類型,比如
type Temp float3
,格式是
{ { function arg1 arg2 }}
。
-
: arg1 == arg2eq
-
: arg1 != arg2ne
-
: arg1 < arg2lt
-
: arg1 <= arg2le
-
: arg1 > arg2gt
-
: arg1 >= arg2ge
eq
函數比較特殊,可以拿多個參數和第一個參數進行比較。
{ { eq arg1 arg2 arg3 arg4}}
邏輯是
arg1==arg2 || arg1==arg3 || arg1==arg4
。
嵌套模闆和布局
嵌套模闆
嵌套模闆可以用做跨模闆的公共部分代碼,比如 header或者 footer。使用嵌套模闆我們就可以避免一點小小的改動就需要修改每個模闆。嵌套模闆定義如下:
{ {define "footer"}}
<footer>
<p>Here is the footer</p>
</footer>
{ {end}}
這裡定義了一個名為
footer
的模闆,可以在其他模闆中使用:
{ {template "footer"}}
模闆之間傳遞變量
模闆action可以使用第二個參數傳遞資料給嵌套的模闆:
// Define a nested template called header
{ {define "header"}}
<h1>{ {.}}</h1>
{ {end}}
// Call template and pass a name parameter
{ {range .Items}}
<div class="item">
{ {template "header" .Name}}
<span class="price">${ {.Price}}</span>
</div>
{ {end}}
這裡我們使用和上面一樣的range周遊items,但是我們會把每個name傳給header模闆。
建立布局
Glob模式通過通配符比對一組檔案名。
template.ParseGlob(pattern string)
會比對所有符合模式的模闆。
template.ParseFiles(files...)
也可以用來解析一組檔案。
、
模闆預設情況下會使用配置的參數檔案名的base name作為模闆名。這意味着
views/layouts/hello.gohtml
的檔案名是
hello.gohtml
,如果模闆中有
{ {define “templateName”}}
的話,那麼
templateName
會用作這個模闆的名字。
模闆可以通過
t.ExecuteTemplate(w, "templateName", nil)
來執行, t是一個類型為
Template
的對象,
w
的類型是
io.Writer
,比如
http.ResponseWriter
,然後是要執行的模闆的名稱,以及要傳入的資料:
main.go
// Omitted imports & package
var LayoutDir string = "views/layouts"
var bootstrap *template.Template
func main() {
var err error
bootstrap, err = template.ParseGlob(LayoutDir + "/*.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
bootstrap.ExecuteTemplate(w, "bootstrap", nil)
}
所有的
.gohtml
檔案都被解析,然後當通路
/
的時候,
bootstrap
會被執行。
views/layouts/bootstrap.gohtml
定義如下:
views/layouts/bootstrap.gohtml
{ {define "bootstrap"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Go Templates</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h1>Filler header</h1>
<p>Filler paragraph</p>
</div>
<!-- jquery & Bootstrap JS -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
</body>
</html>
{ {end}}
模闆調用函數
函數變量 (調用結構體的方法)
我們可以調用模闆中對象的方法傳回資料,下面定義了User類型,以及一個方法:
type User struct {
ID int
Email string
}
func (u User) HasPermission(feature string) bool {
if feature == "feature-a" {
return true
} else {
return false
}
}
當User類型傳給模闆後,我們可以在模闆中調用它的方法:
{ {if .User.HasPermission "feature-a"}}
<div class="feature">
<h3>Feature A</h3>
<p>Some other stuff here...</p>
</div>
{ {else}}
<div class="feature disabled">
<h3>Feature A</h3>
<p>To enable Feature A please upgrade your plan</p>
</div>
{ {end}}
模闆會調用User的HasPermission方法做檢查,并且根據這個傳回結果渲染資料。
函數變量 (調用)
如果有時HasPermission方法的設計不得不需要更改,但是目前的函數方法有不滿足要求,我們可以使用函數(
func(string) bool
)作為User類型的字段,這樣在建立User的時候可以指派不同的函數實作:
// Structs
type ViewData struct {
User User
}
type User struct {
ID int
Email string
HasPermission func(string) bool
}
// Example of creating a ViewData
vd := ViewData{
User: User{
ID: 1,
Email: "[email protected]",
// Create the HasPermission function
HasPermission: func(feature string) bool {
if feature == "feature-b" {
return true
}
return false
},
},
}
// Executing the ViewData with the template
err := testTemplate.Execute(w, vd)
我們需要告訴Go模闆我們想調用這個函數,這裡使用
call
關鍵字。把上面的例子修改如下:
{ {if (call .User.HasPermission "feature-b")}}
<div class="feature">
<h3>Feature B</h3>
<p>Some other stuff here...</p>
</div>
{ {else}}
<div class="feature disabled">
<h3>Feature B</h3>
<p>To enable Feature B please upgrade your plan</p>
</div>
{ {end}}
自定義函數
另外一種方式是使用
template.FuncMap
建立自定義的函數,它建立一個全局的函數,可以在整個應用中使用。
FuncMap
通過
map[string]interface{}
将函數名映射到函數上。注意映射的函數必須隻有一個傳回值,或者有兩個傳回值但是第二個是
error
類型。
// Creating a template with function hasPermission
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(user User, feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).ParseFiles("hello.gohtml")
這個函數
hasPermission
檢查使用者是否有某個權限,它會被儲存在
FuncMap
中。注意自定義的函數必須在調用
ParseFiles()
之前建立。
這個函數在模闆中的使用如下:
{ { if hasPermission .User "feature-a" }}
需要傳入
.User
和
feature-a
參數。
自定義函數 (全局)
我們前面實作的自定義方法需要依賴
.User
類型,很多情況下這種方式工作的很好,但是在一個大型的應用中傳給模闆太多的對象維護起來很困難。我們需要改變自定義的函數,讓它無需依賴User對象。
和上面的實作類似,我們建立一個預設的
hasPermission
函數,這樣可以正常解析模闆。
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
return false
},
}).ParseFiles("hello.gohtml")
這個函數在
main()
中或者某處建立,并且保證在解析檔案之前放入到 hello.gohtml 的function map中。這個預設的函數總是傳回false,但是不管怎樣,函數是已定義的,而且不需要User,模闆也可以正常解析。
下一個技巧就是重新定義
hasPermission
函數。這個函數可以使用User對象的資料,但是它是在Handler進行中使用的,而不是傳給模闆,這裡采用的是閉包的方式。是以在模闆執行之前你死有機會重新定義函數的。
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
user := User{
ID: 1,
Email: "[email protected]",
}
vd := ViewData{}
err := testTemplate.Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
在這個Handler中User被建立,ViewData使用這個User對象。
hasPermission
采用閉包的方式重新定義了函數。
{ {if hasPermission "feature-a"}}
的的确确沒有傳入User參數。
第三方自定義函數
除了官方的預定義的函數外,一些第三方也定義了一些函數,你可以使用這些庫,避免重複造輪子。
比如sprig庫,定義了很多的函數:
-
String Functions
:
trim
,
wrap
,
randAlpha
,
plural
, etc.
- String List Functions:
,splitList
, etc.sortAlpha
-
Math Functions
:
add
,
max
,
mul
- Integer Slice Functions:
,until
untilStep
- Date Functions:
,now
, etc.date
- Defaults Functions:
,default
,empty
,coalesce
,toJson
,toPrettyJson
,toRawJson
ternary
- Encoding Functions:
,b64enc
, etc.b64dec
- Lists and List Functions:
,list
,first
, etc.uniq
- Dictionaries and Dict Functions:
,get
,set
,dict
,hasKey
,pluck
, etc.deepCopy
- Type Conversion Functions:
,atoi
,int64
, etc.toString
- File Path Functions:
,base
,dir
,ext
,clean
isAbs
- Flow Control Functions:
fail
- Advanced Functions
- UUID Functions:
uuidv4
- OS Functions:
,env
expandenv
- Version Comparison Functions:
,semver
semverCompare
- Reflection:
,typeOf
,kindIs
, etc.typeIsLike
- Cryptographic and Security Functions:
,derivePassword
,sha256sum
, etc.genPrivateKey