深入淺出 Golang 標準庫 text-template 包
【導讀】go 語言標準庫 text/template 在 web 編程項目中經常用到,本文詳細介紹了這個包的用法。
官方定義:
Package template implements data-driven templates for generating textual output.
template 包是數據驅動的文本輸出模板,其實就是在寫好的模板中填充數據。
模板
什麼是模板?
下面是一個簡單的模板示例:
// 模板定義
tepl := "My name is {{ . }}"
// 解析模板
tmpl, err := template.New("test").Parse(tepl)
// 數據驅動模板
data := "jack"
err = tmpl.Execute(os.Stdout, data)
{{和}} 中間的句號 .
代表傳入模板的數據,根據傳入的數據不同渲染不同的內容。
.
可以代表 go 語言中的任何類型,如結構體、哈希等。
至於 {{和}} 包裹的內容統稱爲 action,分爲兩種類型:
-
數據求值(data evaluations)
-
控制結構(control structures)
action 求值的結果會直接複製到模板中,控制結構和我們寫 Go 程序差不多,也是條件語句、循環語句、變量、函數調用等等。..
將模板成功解析(Parse)後,可以安全地在併發環境中使用,如果輸出到同一個 io.Writer
數據可能會重疊(因爲不能保證併發執行的先後順序)。
Actions
模板中的 action 並不多,我們一個一個看。
註釋
{{/* comment */}}
裁剪空格
// 裁剪 content 前後的空格
{{- content -}}
// 裁剪 content 前面的空格
{{- content }}
// 裁剪 content 後面的空格
{{ content -}}
文本輸出
{{ pipeline }}
pipeline 代表的數據會產生與調用 fmt.Print
函數類似的輸出,例如整數類型的 3 會轉換成字符串 "3" 輸出。
條件語句
{{ 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 }}
{{ if pipeline }} T1 {{ else if pipeline }} T2 {{ else }} T0 {{ end }}
如果 pipeline 的值爲空,不會輸出 T1,除此之外 T1 都會被輸出。
空值有 false、0、任意 nil 指針、接口值、數組、切片、字典和空字符串 ""
(長度爲 0 的字符串)。
循環語句
{{ range pipeline }} T1 {{ end }}
// 這個 else 比較有意思,如果 pipeline 的長度爲 0 則輸出 else 中的內容
{{ range pipeline }} T1 {{ else }} T0 {{ end }}
// 獲取容器的下標
{{ range $index, $value := pipeline }} T1 {{ end }}
pipeline 的值必須是數組、切片、字典和通道中的一種,即可迭代類型的值,根據值的長度輸出多個 T1。
define
{{ define "name" }} T {{ end }}
定義命名爲 name 的模板。
template
{{ template "name" }}
{{ template "name" pipeline }}
引用命名爲 name 的模板。
block
{{ block "name" pipeline }} T1 {{ end }}
block 的語義是如果有命名爲 name 的模板,就引用過來執行,如果沒有命名爲 name 的模板,就是執行自己定義的內容。
也就是多做了一步模板是否存在的判斷,根據這個結果渲染不同的內容。
with
{{ with pipeline }} T1 {{ end }}
// 如果 pipeline 是空值則輸出 T0
{{ with pipeline }} T1 {{ else }} T0 {{ end }}
{{ with arg }}
. // 此時 . 就是 arg
{{ end }}
with 創建一個新的上下文環境,在此環境中的 .
與外面的 .
無關。
參數
參數的值有多種表現形式,可以求值任何類型,包括函數、指針(指針會自動間接取值到原始的值):
-
布爾、字符串、字符、浮點數、複數的行爲和 Go 類似
-
關鍵字
nil
代表 go 語言中的nil
-
字符句號 . 代表值的結果
-
以 $ 字符開頭的變量則爲變量對應的值
-
結構體的字段表示爲
.Field
,結果是 Field 的值,支持鏈式調用.Field1.Field2
-
字典的 key 表示爲
.Key
結果是 Key 對應的值 -
如果是結構體的方法集中的方法 .Method 結果是方法調用後返回的值(The result is the value of invoking the method with dot as the receiver)**
-
方法要麼只有一個任意類型的返回值要麼第二個返回值爲 error,不能再多了,如果 error 不爲 nil,會直接報錯,停止模板渲染
-
方法調用的結果可以繼續鏈式調用
.Field1.Key1.Method1.Field2.Key2.Method2
-
聲明變量方法集也可以調用
$x.Method1.Field
-
用括號將調用分組
print (.Func1 arg1) (.Func2 arg2)
或(.StructValuedMethod "arg").Field
這裏最難懂的可能就是函數被調用的方式,如果訪問結構體方法集中的函數和字段中的函數,此時的行爲有什麼不同?
寫個 demo 測一下:
type T struct {
Add func(int) int
}
func (t *T) Sub(i int) int {
log.Println("get argument i:", i)
return i - 1
}
func arguments() {
ts := &T{
Add: func(i int) int {
return i + 1
},
}
tpl := `
// 只能使用 call 調用
call field func Add: {{ call .ts.Add .y }}
// 直接傳入 .y 調用
call method func Sub: {{ .ts.Sub .y }}
`
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
"y": 3,
"ts": ts,
})
}
output:
call field func Add: 4
call method func Sub: 2
可以得出結論:如果函數是結構體中的函數字段,該函數不會自動調用,只能使用內置函數 call
調用。
如果函數是結構體方法集中的方法,會自動調用該方法,並且會將返回值賦值給 .
,如果函數返回新的結構體、map,可以繼續鏈式調用。
變量
action 中的 pipeline 可以初始化變量存儲結果,語法也很簡單:
$variable = pipeline
此時,這個 action 聲明瞭一個變量而沒有產生任何輸出。
range 循環可以聲明兩個變量:
range $index, $element := pipeline
在 if、with 和 range 中,變量的作用域拓展到 {{end}} 所在的位置。
如果不是控制結構,聲明的變量的作用域會擴展到整個模板。
例如在模板開始時聲明變量:
{{ $pages := .pagination.Pages }}
{{ $current := .pagination.Current }}
在渲染開始的時候,$
變量會被替換成 .
開頭的值,例如 $pages
會被替換成 .pagenation.Pages
。所以在模板間的相互引用不會傳遞變量,變量只在某個特定的作用域中產生作用。
函數
模板渲染時會在兩個地方查找函數:
-
自定義的函數 map
-
全局函數 map,這些函數是模板內置的
自定義函數使用 func (t *Template) Funcs(funcMap FuncMap) *Template
註冊。
全局函數列表:
and
返回參數之間 and 布爾操作的結果,其實就是 JavaScript 中的邏輯操作符 &&
,返回第一個能轉換成 false 的值,在 Go 中就是零值,如果都爲 true 返回最後一個值。
tpl := "{{ and .x .y .z }}"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
"x": 1,
"y": 0,
"z": 3,
})
output:
0
or
邏輯操作符 ||
,返回第一個能轉換成 true 的值,在 Go 中就是非零值,如果都爲 false 返回最後一個值。
tpl := "{{ or .x .y .z }}"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
"x": 1,
"y": 0,
"z": 3,
})
output:
1
call
返回調用第一個函數參數的結果,函數必須有一個或兩個回值(第二個返回值必須是 error,如果值不爲 nil 會停止模板渲染)
tpl := "call: {{ call .x .y .z }} \n"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
"x": func(x, y int) int { return x+y},
"y": 2,
"z": 3,
})
output:
5
html
返回轉義後的 HTML 字符串,這個函數不能在 html/template
中使用。
js
返回轉義後的 JavaScript 字符串。
index
在第一個參數是 array、slice、map 時使用,返回對應下標的值。
index x 1 2 3
等於 x[1][2][3]
。
len
返回複合類型的長度。
not
返回布爾類型參數的相反值。
等於 fmt.Sprint
。
printf
等於 fmt.Sprintf
。
println
等於 fmt.Sprintln
。
urlquery
對字符串進行 url Query 轉義,不能在 html/template
包中使用。
// URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query.
func URLQueryEscaper(args ...interface{}) string {
return url.QueryEscape(evalArgs(args))
}
從源碼可以看到這個函數直接調用 url.QueryEscape
對字符串進行轉義,並沒有什麼神祕的。
比較函數
-
eq
: == -
ge
: >= -
gt
: > -
le
: <= -
lt
: < -
ne
: !=
分析兩個源碼:
// eq evaluates the comparison a == b || a == c || ...
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
v1 := indirectInterface(arg1)
k1, err := basicKind(v1)
if err != nil {
return false, err
}
if len(arg2) == 0 {
return false, errNoComparison
}
for _, arg := range arg2 {
v2 := indirectInterface(arg)
k2, err := basicKind(v2)
if err != nil {
return false, err
}
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
case k1 == uintKind && k2 == intKind:
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
default:
return false, errBadComparison
}
} else {
switch k1 {
case boolKind:
truth = v1.Bool() == v2.Bool()
case complexKind:
truth = v1.Complex() == v2.Complex()
case floatKind:
truth = v1.Float() == v2.Float()
case intKind:
truth = v1.Int() == v2.Int()
case stringKind:
truth = v1.String() == v2.String()
case uintKind:
truth = v1.Uint() == v2.Uint()
default:
panic("invalid kind")
}
}
if truth {
return true, nil
}
}
return false, nil
}
// ne evaluates the comparison a != b.
func ne(arg1, arg2 reflect.Value) (bool, error) {
// != is the inverse of ==.
equal, err := eq(arg1, arg2)
return !equal, err
}
eq 先判斷接口類型是否相等,然後判斷值是否相等,沒什麼特殊的地方。
ne 更是簡單的調用 eq,然後取反。
ge、gt、le、lt 與 eq 類似,先判斷類型,然後判斷大小。
嵌套模板
下面是一個更復雜的例子:
// 加載模板
template.ParseFiles("templates/")
// 加載多個模板到一個命名空間(同一個命名空間的模塊可以互相引用)
template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
// must 加載失敗時 panic
tmpl := template.Must(template.ParseFiles("layout.html"))
// 執行加載後的模板文件,默認執行第一個
tmpl.Execute(w, "test")
// 如果 tmpl 中有很多個模板,可以指定要執行的模板名
tmpl.ExecuteTemplate(w, "layout", "Hello world")
ExecuteTemplate
指定的名字就是模板文件中 define "name"
的 name。
總結
Parse
系列函數初始化的 Template
類型實例。
Execute
系列函數則將數據傳遞給模板渲染最終的字符串。
模板本質上就是 Parse
函數加載多個文件到一個 Tempalte
類型實例中,解析文件中的 define 關鍵字註冊命名模板,命名模板之間可以使用 template
互相引用,Execute
傳入對應的數據渲染。
轉自:
juejin.cn/post/6844903762901860360
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/k__ynwBkIcwEK7HAL4WUCA