Go 語言標準庫 text/template 包深刻淺出

目錄

官方定義:html

Package template implements data-driven templates for generating textual output.數組

template 包是數據驅動的文本輸出模板,其實就是在寫好的模板中填充數據。安全

模板

什麼是模板?bash

下面是一個簡單的模板示例:併發

// 模板定義
tepl := "My name is {{ . }}"

// 解析模板
tmpl, err := template.New("test").Parse(tepl)

// 數據驅動模板
data := "jack"
err = tmpl.Execute(os.Stdout, data)
複製代碼

{{ 和 }} 中間的句號 . 表明傳入模板的數據,根據傳入的數據不一樣渲染不一樣的內容。less

. 能夠表明 go 語言中的任何類型,如結構體、哈希等。函數

至於 {{ 和 }} 包裹的內容統稱爲 action,分爲兩種類型:ui

  • 數據求值(data evaluations)
  • 控制結構(control structures)

action 求值的結果會直接複製到模板中,控制結構和咱們寫 Go 程序差很少,也是條件語句、循環語句、變量、函數調用等等...lua

將模板成功解析(Parse)後,能夠安全地在併發環境中使用,若是輸出到同一個 io.Writer 數據可能會重疊(由於不能保證併發執行的前後順序)。url

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

返回布爾類型參數的相反值。

print

等於 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 傳入對應的數據渲染。

相關文章
相關標籤/搜索