4.3 模板(text/template)

text/template是Go语言标准库,实现数据驱动模板以生成文本输出,可以理解为一组文字按照特定格式动态嵌入另一组文字中。

还有个处理html文字的模板(html/template),感兴趣的可以了解下。

简单字符

示例

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. name := "world"
  13. tmpl, err := template.New("test").Parse("hello, {{.}}") //建立一个模板,内容是"hello, {{.}}"
  14. CheckErr(err)
  15. err = tmpl.Execute(os.Stdout, name) //将string与模板合成,变量name的内容会替换掉{{.}}
  16. //合成结果放到os.Stdout里 输出
  17. CheckErr(err)
  18. }

输出: hello, world

模板的输入文本是任何格式的UTF-8编码文本。 {{ 和 }} 包裹的内容统称为 action,分为两种类型:

  • 数据求值(data evaluations)
  • 控制结构(control structures)

action 求值的结果会直接复制到模板中,控制结构和我们写 Go 程序差不多,也是条件语句、循环语句、变量、函数调用等等…

将模板成功解析(Parse)后,可以安全地在并发环境中使用,如果输出到同一个 io.Writer 数据可能会重叠(因为不能保证并发执行的先后顺序)。

这里{{和}}中间的句号(.)代表传入的数据,数据不同渲染不同,可以代表 go 语言中的任何类型,如结构体、哈希等。

struct

示例

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. type Inventory struct {
  13. Material string
  14. Count uint
  15. }
  16. sweaters := Inventory{"wool", 17}
  17. tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
  18. CheckErr(err)
  19. err = tmpl.Execute(os.Stdout, sweaters)
  20. CheckErr(err)
  21. }

输出:17 items are made of wool

模板文件

新建一个模板文件 demo.go.tpl

内容

  1. {{.Count}} items are made of {{.Material}}

上面示例可改为

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. type Inventory struct {
  13. Material string
  14. Count uint
  15. }
  16. sweaters := Inventory{"wool", 17}
  17. tmpl, err := template.ParseFiles("demo.go.tpl")
  18. CheckErr(err)
  19. err = tmpl.Execute(os.Stdout, sweaters)
  20. CheckErr(err)
  21. }

输出:17 items are made of wool

多模板

示例

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "text/template"
  6. )
  7. func CheckErr(err error) {
  8. if err != nil {
  9. panic(err)
  10. }
  11. }
  12. func main() {
  13. type Inventory struct {
  14. Material string
  15. Count uint
  16. }
  17. sweaters := Inventory{"wool", 17}
  18. tmpl, err := template.New("T2").Parse("{{.Count}} items are made of")
  19. tmpl, err = tmpl.New("test").Parse("{{.Count}} items are made of {{.Material}}")
  20. CheckErr(err)
  21. err = tmpl.ExecuteTemplate(os.Stdout, "T2", sweaters) //可以选取模板
  22. CheckErr(err)
  23. fmt.Println("")
  24. fmt.Println(tmpl.Name())
  25. tmpl = tmpl.Lookup("test") //切换模板,必须要有返回,否则不生效
  26. fmt.Println(tmpl.Name())
  27. }

输出:

17 items are made of test test

输出到指定文件

示例

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. type Inventory struct {
  13. Material string
  14. Count uint
  15. }
  16. sweaters := Inventory{"wool", 17}
  17. tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
  18. CheckErr(err)
  19. file, err := os.OpenFile("demo.txt", os.O_CREATE|os.O_WRONLY, 0755)
  20. CheckErr(err)
  21. err = tmpl.Execute(file, sweaters)
  22. CheckErr(err)
  23. }

打开文件 demo.txt,内容为: 17 items are made of wool

循环操作

使用方法

  1. {{range .Field}}
  2. {{.ChildFieldOne}} -- {{.ChildFieldTwo }}
  3. {{ end }}

示例

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. type Inventory struct {
  13. Material string
  14. Count uint
  15. }
  16. type NewInventory struct {
  17. Fields []Inventory
  18. }
  19. sweaters := NewInventory{
  20. Fields: []Inventory{
  21. Inventory{Material: "wool", Count: 19},
  22. Inventory{Material: "wooltwo", Count: 20},
  23. }}
  24. var Text = `
  25. {{range .Fields }}
  26. Material: {{.Material}} - Count:{{.Count}}
  27. {{ end }}
  28. `
  29. tmpl, err := template.New("test").Parse(Text)
  30. CheckErr(err)
  31. err = tmpl.Execute(os.Stdout, sweaters)
  32. CheckErr(err)
  33. }

模板函数

即:可以对某个字段使用函数操作。适用于稍微复杂的字段处理。

  1. type FuncMap map[string]interface{}
  2. t = t.Funcs(template.FuncMap{"handleFieldName": HandleFunc})

内置模板函数:

  1. var builtins = FuncMap{
  2. "and": and,
  3. "call": call,
  4. "html": HTMLEscaper,
  5. "index": index,
  6. "js": JSEscaper,
  7. "len": length,
  8. "not": not,
  9. "or": or,
  10. "print": fmt.Sprint,
  11. "printf": fmt.Sprintf,
  12. "println": fmt.Sprintln,
  13. "urlquery": URLQueryEscaper,
  14. }

示例

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func CheckErr(err error) {
  7. if err != nil {
  8. panic(err)
  9. }
  10. }
  11. func main() {
  12. type Inventory struct {
  13. Material string
  14. Count uint
  15. }
  16. type NewInventory struct {
  17. Fields []Inventory
  18. }
  19. sweaters := NewInventory{
  20. Fields: []Inventory{
  21. Inventory{Material: "wool", Count: 19},
  22. Inventory{Material: "wooltwo", Count: 20},
  23. }}
  24. var Text = `
  25. {{range .Fields }}
  26. Material: {{.Material | handleString}} - Count:{{.Count | handleInt }}
  27. {{ end }}
  28. `
  29. tmpl, err := template.New("test").Funcs(template.FuncMap{"handleString": handleString, "handleInt": handleInt}).Parse(Text)
  30. CheckErr(err)
  31. err = tmpl.Execute(os.Stdout, sweaters)
  32. CheckErr(err)
  33. }
  34. func handleInt(number uint) uint {
  35. return number + 10
  36. }
  37. func handleString(field string) string {
  38. return " string is: " + field
  39. }

输出:

Material: string is: wool - Count:29

Material: string is: wooltwo - Count:30

Actions

注释

  1. {{*/\* comment \*/*}}

裁剪空格

  1. // 裁剪 content 前后的空格
  2. {{- content -}}
  3. // 裁剪 content 前面的空格
  4. {{- content }}
  5. // 裁剪 content 后面的空格
  6. {{ content -}}

管道函数

  1. 用法1
  2. {{FuncName1}}
  3. 此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1()”,不传递任何参数)并输出其返回值。
  4. 用法2
  5. {{FuncName1 "参数值1" "参数值2"}}
  6. 此标签将调用“FuncName1("参数值1", "参数值2")”,并输出其返回值
  7. 用法3
  8. {{.Admpub|FuncName1}}
  9. 此标签将调用名称为“FuncName1”的模板函数(等同于执行“FuncName1(this.Admpub)”,将竖线“|”左边的“.Admpub”变量值作为函数参数传送)并输出其返回值。

文本输出

  1. {{ pipeline }}

pipeline 代表的数据会产生与调用 fmt.Print 函数类似的输出,例如整数类型的 3 会转换成字符串 “3” 输出。

条件语句

  1. {{ if pipeline }} T1 {{ end }}
  2. {{ if pipeline }} T1 {{ else }} T0 {{ end }}
  3. {{ if pipeline }} T1 {{ else if pipeline }} T0 {{ end }}
  4. *// 上面的语法其实是下面的简写*
  5. {{ if pipeline }} T1 {{ else }}{{ if pipeline }} T0 { {end }}{{ end }}
  6. {{ if pipeline }} T1 {{ else if pipeline }} T2 {{ else }} T0 {{ end }}

循环语句

  1. {{ range pipeline }} T1 {{ end }}
  2. *// 这个 else 比较有意思,如果 pipeline 的长度为 0 则输出 else 中的内容*
  3. {{ range pipeline }} T1 {{ else }} T0 {{ end }}
  4. *// 获取容器的下标*
  5. {{ range $index, $value := pipeline }} T1 {{ end }}

pipeline 的值必须是数组、切片、字典和通道中的一种,即可迭代类型的值,根据值的长度输出多个 T1。

define

  1. {{ define "name" }} T {{ end }}

定义命名为 name 的模板。

template

  1. {{ template "name" }}
  2. {{ template "name" pipeline }}

引用命名为 name 的模板。

block

  1. {{ block "name" pipeline }} T1 {{ end }}

block 的语义是如果有命名为 name 的模板,就引用过来执行,如果没有命名为 name 的模板,就是执行自己定义的内容。

也就是多做了一步模板是否存在的判断,根据这个结果渲染不同的内容。

with

  1. {{ with pipeline }} T1 {{ end }}
  2. // 如果 pipeline 是空值则输出 T0
  3. {{ with pipeline }} T1 {{ else }} T0 {{ end }}
  4. {{ with arg }}
  5. . // 此时 . 就是 arg
  6. {{ 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

变量

action 中的 pipeline 可以初始化变量存储结果,语法也很简单:

  1. $variable = pipeline

此时,这个 action 声明了一个变量而没有产生任何输出。

range 循环可以声明两个变量:

  1. range $index, $element := pipeline

在 if、with 和 range 中,变量的作用域拓展到 {{ end }} 所在的位置。

如果不是控制结构,声明的变量的作用域会扩展到整个模板。

例如在模板开始时声明变量:

  1. {{ $pages := .pagination.Pages }}
  2. {{ $current := .pagination.Current }}

在渲染开始的时候,$ 变量会被替换成 . 开头的值,例如 $pages 会被替换成 .pagenation.Pages。所以在模板间的相互引用不会传递变量,变量只在某个特定的作用域中产生作用。

函数

模板渲染时会在两个地方查找函数:

  • 自定义的函数 map
  • 全局函数 map,这些函数是模板内置的

自定义函数使用 func (t *Template) Funcs(funcMap FuncMap) *Template 注册。

全局函数列表:

and

返回参数之间 and 布尔操作的结果,其实就是 JavaScript 中的逻辑操作符 &&,返回第一个能转换成 false 的值,在 Go 中就是零值,如果都为 true 返回最后一个值。

  1. tpl := "{{ and .x .y .z }}"
  2. t, _ := template.New("test").Parse(tpl)
  3. t.Execute(os.Stdout, map[string]interface{}{
  4. "x": 1,
  5. "y": 0,
  6. "z": 3,
  7. })
  8. output:
  9. 0

or

逻辑操作符 ||,返回第一个能转换成 true 的值,在 Go 中就是非零值,如果都为 false 返回最后一个值。

  1. tpl := "{{ or .x .y .z }}"
  2. t, _ := template.New("test").Parse(tpl)
  3. t.Execute(os.Stdout, map[string]interface{}{
  4. "x": 1,
  5. "y": 0,
  6. "z": 3,
  7. })
  8. output:
  9. 1

call

返回调用第一个函数参数的结果,函数必须有一个或两个回值(第二个返回值必须是 error,如果值不为 nil 会停止模板渲染)

  1. tpl := "call: {{ call .x .y .z }} \n"
  2. t, _ := template.New("test").Parse(tpl)
  3. t.Execute(os.Stdout, map[string]interface{}{
  4. "x": func(x, y int) int { return x+y},
  5. "y": 2,
  6. "z": 3,
  7. })
  8. output:
  9. 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 包中使用。

  1. // URLQueryEscaper returns the escaped value of the textual representation of
  2. // its arguments in a form suitable for embedding in a URL query.
  3. func URLQueryEscaper(args ...interface{}) string {
  4. return url.QueryEscape(evalArgs(args))
  5. }

从源码可以看到这个函数直接调用 url.QueryEscape 对字符串进行转义,并没有什么神秘的。

比较函数

  • eq: ==
  • ge: >=
  • gt: >
  • le: <=
  • lt: <
  • ne: !=

分析两个源码:

  1. // eq evaluates the comparison a == b || a == c || ...
  2. func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
  3. v1 := indirectInterface(arg1)
  4. k1, err := basicKind(v1)
  5. if err != nil {
  6. return false, err
  7. }
  8. if len(arg2) == 0 {
  9. return false, errNoComparison
  10. }
  11. for _, arg := range arg2 {
  12. v2 := indirectInterface(arg)
  13. k2, err := basicKind(v2)
  14. if err != nil {
  15. return false, err
  16. }
  17. truth := false
  18. if k1 != k2 {
  19. // Special case: Can compare integer values regardless of type's sign.
  20. switch {
  21. case k1 == intKind && k2 == uintKind:
  22. truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
  23. case k1 == uintKind && k2 == intKind:
  24. truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
  25. default:
  26. return false, errBadComparison
  27. }
  28. } else {
  29. switch k1 {
  30. case boolKind:
  31. truth = v1.Bool() == v2.Bool()
  32. case complexKind:
  33. truth = v1.Complex() == v2.Complex()
  34. case floatKind:
  35. truth = v1.Float() == v2.Float()
  36. case intKind:
  37. truth = v1.Int() == v2.Int()
  38. case stringKind:
  39. truth = v1.String() == v2.String()
  40. case uintKind:
  41. truth = v1.Uint() == v2.Uint()
  42. default:
  43. panic("invalid kind")
  44. }
  45. }
  46. if truth {
  47. return true, nil
  48. }
  49. }
  50. return false, nil
  51. }
  52. // ne evaluates the comparison a != b.
  53. func ne(arg1, arg2 reflect.Value) (bool, error) {
  54. // != is the inverse of ==.
  55. equal, err := eq(arg1, arg2)
  56. return !equal, err
  57. }

eq 先判断接口类型是否相等,然后判断值是否相等,没什么特殊的地方。

ne 更是简单的调用 eq,然后取反。

ge、gt、le、lt 与 eq 类似,先判断类型,然后判断大小。

嵌套模板

下面是一个更复杂的例子:

  1. // 加载模板
  2. template.ParseFiles("templates/")
  3. // 加载多个模板到一个命名空间(同一个命名空间的模块可以互相引用)
  4. template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
  5. // must 加载失败时 panic
  6. tmpl := template.Must(template.ParseFiles("layout.html"))
  7. // 执行加载后的模板文件,默认执行第一个
  8. tmpl.Execute(w, "test")
  9. // 如果 tmpl 中有很多个模板,可以指定要执行的模板名
  10. tmpl.ExecuteTemplate(w, "layout", "Hello world")

ExecuteTemplate 指定的名字就是模板文件中 define "name" 的 name。

参考:

https://golang.org/pkg/text/template/

https://juejin.im/post/5c403b98f265da612d1984c9

links