( template 包的文档可以从 golang.org/pkg/template/ 获取)

在上一节中,我们使用了模板去合并结构体与 html 模板中的数据。这对于构建 web 应用程序确实非常有用,但是模板技术比这更通用:数据驱动模板可以用于生成文本输出,HTML 仅仅是其中一个特例。

通过执行 template 将模板与数据结构合并,在大多数情况下是一个结构体或者一个结构体的切片。它可以重写一段文本,通过传递给 templ.Execute () 的数据项进行替换生成新的内容。只有能被导出的数据项才可以用于模板合并。操作可以是数据评估或控制结构,并通过 「{{」和「}}」定义。数据项可以是值或者指针;接口隐藏了间接引用(译者注:个人觉得应该是接口将传递的是值还是指针忽略了,因为实际使用中,无论是指针还是值都可以直接通过 .Title 来使用)。

15.7.1. 字段替代: {{.FieldName}}

在模板中包含字段的内容,需要把它放在一个双大括号中,并且在变量的前面加上一个点,例如: 如果 Name 是一个结构体中的字段,并且它的值需要在合并时替换,那么在模板中包含文本 {{.Name}} ;当 Name 是一个 map 的索引时,也可以这样使用。使用 template.New 创建一个新的模板,需要一个字符串的参数来作为模板的名称。正如我们在 15.5 章节 已经遇到的,Parse 方法通过解析一些模板定义的字符串来生成一个 template 作为内部表示,当参数是一个定义好的模板文件的路径时,使用 ParseFile。当解析出现问题的时候,第二个参数会返回 Error != nil 。在最后一步,数据结构通过 execute 方法与模板合并,并且将一个 io.Writer 写入到它的第一个参数;可以再次返回错误。这将在下面程序中说明,输出的结果通过 os.Stdout 显示到控制台:

示例 15.13—template_field.go:

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. type Person struct {
  7. Name string
  8. nonExportedAgeField string // 译者添加: 原文中没有定义这个,代码会报错
  9. }
  10. func main() {
  11. t := template.New("hello")
  12. t, _ = t.Parse("hello {{.Name}}!")
  13. p := Person{Name:"Mary", nonExportedAgeField: "31"} // data
  14. if err := t.Execute(os.Stdout, p); err != nil {
  15. fmt.Println("There was an error:", err.Error())
  16. }
  17. }
  18. // Output: hello Mary!

我们的结构体包含了一个不能导出的字段,并且当我们尝试通过一个定义字符串去合并他时,像下面这样:

  1. t, _ = t.Parse("your age is {{.nonExportedAgeField}}!")

发生下面错误:There was an error: template:nonexported template hello:1: can’t evaluate field nonExportedAgeField in type main.Person.

译者注: 可能是新版本已经更新了这部分代码,现在(go1.10.1 )已经不会出现上面的错误,如果在结构体中没有定义 nonExportedAgeField 字段,在 p := Person{Name:"Mary", nonExportedAgeField: "31"} 的时候就直接编译不过去了,如果定义了 nonExportedAgeField 字段(小写开头不能导出),也不会报错,只是模板中定义的 {{.nonExportedAgeField}} 不显示内容。但是直接使用 {{ . }} ,不管字段是否可以导出,会将两个字段全部输出。

你可以直接在 Execute() 中使用 {{.}} 直接显示两个参数,结果就是: hello {Mary 31}!

当模板应用在浏览器中时,要先用 html 过滤器去过滤输出的内容,像这样: {{html .}} ,或者使用一个 FieldName {{ .FieldName |html }}

|html 部分告诉 template 引擎在输出 FieldName 的值之前要通过 html 格式化它。他会转义特殊的 html 字符( 如:会将 > 替换成 > ), 这样可以防止用户的数据破坏 HTML 表单。

译者注: |html 用起来就和 Linux 的 | (管道)类似,将前面命令的输出作为 | 后面命令的输入

15.7.2. 模板验证

检查模板的语法是否定义正确,对 Parse 的结果执行 Must 函数。在下面的示例中 tOK 是正确, tErr 的验证会出现错误并会导致一个运行时恐慌(panic)!

示例 15.14—template_validation.go:

  1. package main
  2. import (
  3. "text/template"
  4. "fmt"
  5. )
  6. func main() {
  7. tOk := template.New("ok")
  8. // 一个有效的模板,所以 Must 时候不会出现恐慌(panic)
  9. template.Must(tOk.Parse("/*这是一个注释 */ some static text: {{ .Name }}"))
  10. fmt.Println("The first one parsed OK.")
  11. fmt.Println("The next one ought to fail.")
  12. tErr := template.New("error_template")
  13. template.Must(tErr.Parse("some static text {{ .Name }"))
  14. }

/* Output:

  1. The first one parsed OK.
  2. The next one ought to fail.
  3. panic: template: error_template:1: unexpected "}" in command

*/

模板语法中的错误应该不常见,因为会使用像 13.3 章节 中的 defer/recover 机制去报告这个错误并纠正它。

下面的三个基本函数在代码中经常被链接使用,就像:

  1. var strTempl = template.Must(template.New(“TName”).Parse(strTemplateHTML))

练习 15.7: template_validation_recover.go

在上面的示例中,实现 defer/recover 机制。

15.7.3. If-else

输出由 Execute 生成的模板结果中,包含了静态文本和在 {{}} 中包含的文本,它们被称为一个管道。例如: 运行这个代码 (示例程序 15.15 pipeline1.go ):

  1. t := template.New("template test")
  2. t = template.Must(t.Parse("This is just static text. \n{{\"This is pipeline data—because it is evaluated within the double braces.\"}} {{`So is this, but within reverse quotes.`}}\n"))
  3. t.Execute(os.Stdout, nil)

获得这个输出结果:

  1. This is just static text.
  2. This is pipeline databecause it is evaluated within the double braces. So is this, but within reverse quotes.

现在我们可以使用 if-else-end 来调整管道数据的输出: 如果管道是空的,就像:

  1. in: {{if ``}} Will not print. {{end}}

if 条件的判断结果是 false ,并不会输出任何任内容,但是这个:

  1. {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}

Print IF part 将被输出。上面的内容在下面的程序中被说明:

示例 15.16—template_ifelse.go:

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func main() {
  7. tEmpty := template.New("template test")
  8. // if 是一个空管道时的内容
  9. tEmpty = template.Must(tEmpty.
  10. Parse("Empty pipeline if demo: {{if ``}} Will not print. {{end}}\n"))
  11. tEmpty.Execute(os.Stdout, nil)
  12. tWithValue := template.New("template test")
  13. // 如果条件满足,则为非空管道
  14. tWithValue = template.Must(tWithValue.
  15. Parse("Non empty pipeline if demo: {{if `anything`}} Will print. {{end}}\n"))
  16. tWithValue.Execute(os.Stdout, nil)
  17. tIfElse := template.New("template test")
  18. // 如果条件满足,则为非空管道
  19. tIfElse = template.Must(tIfElse.
  20. Parse("if-else demo: {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}\n"))
  21. tIfElse.Execute(os.Stdout, nil)
  22. }

/* 输出:

  1. Empty pipeline if demo:
  2. Non empty pipeline if demo: Will print.
  3. if-else demo: Print IF part.

*/

15.7.4. 点与 with-end

在 Go 模板中使用 (.) : 他的值 {{.}} 被设置为当前管道的值。

with 语句将点的值设置为管道的值。如果管道是空的,就会跳过 with 到 end 之前的任何内容;当嵌套使用时,点会从最近的范围取值。在下面这个程序中会说明:

示例 15.17—template_with_end.go:

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t, _ = t.Parse("{{with `hello`}}{{.}}{{end}}!\n")
  9. t.Execute(os.Stdout, nil)
  10. t, _ = t.Parse("{{with `hello`}}{{.}} {{with `Mary`}}{{.}}{{end}} {{end}}!\n")
  11. t.Execute(os.Stdout, nil)
  12. }

/* 输出:

  1. hello!
  2. hello Mary!

*/

15.7.5. 模板变量 $

你可以在变量名前加一个「$」符号来为模板中的管道创建一个局部变量。变量名称只能由字母、数字、下划线组成。在下面的示例中,我使用了几种可以使用的变量名称。

示例 15.18—template_variables.go:

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t = template.Must(t.Parse("{{with $3 := `hello`}}{{$3}}{{end}}!\n"))
  9. t.Execute(os.Stdout, nil)
  10. t = template.Must(t.Parse("{{with $x3 := `hola`}}{{$x3}}{{end}}!\n"))
  11. t.Execute(os.Stdout, nil)
  12. t = template.Must(t.Parse("{{with $x_1 := `hey`}}{{$x_1}} {{.}} {{$x_1}} {{end}}!\n"))
  13. t.Execute(os.Stdout, nil)
  14. }

/* 输出:

  1. hello!
  2. hola!
  3. hey hey hey!*/

*/

15.7.6. Range-end

这个构造的格式:

  1. {{range pipeline}} T1 {{else}} T0 {{end}}

range 在循环的集合中使用: 管道的值必须是一个数组、切片或者 map 。如果管道的值的长度为零,点不会被影响并且 T0 会被执行;否则将点设置成拥有连续元素的数组、切片或者 map, T1 就会被执行。

  1. 如果它是模板: {{range .}}
  2. {{.}}
  3. {{end}}
  4. 然后是这个代码: s := []int{1,2,3,4}
  5. t.Execute(os.Stdout, s)
  6. 将会输出:
  7. 1
  8. 2
  9. 3
  10. 4

可以查看 20.7 章节,它是一个更有用的示例,其中来自 App Engine 数据存储的数据通过一个模板展示:

  1. {{range .}}
  2. {{with .Author}}
  3. <p><b>{{html .}}</b> wrote:</p>
  4. {{else}}
  5. <p>An anonymous person wrote:</p>
  6. {{end}}
  7. <pre>{{html .Content}}</pre>
  8. <pre>{{html .Date}}</pre>
  9. {{end}}

range . 这里循环了一个结构体的切片,每个结构体都包含了一个 Author、Content 和 Date 字段。

15.7.7. 预定义模板函数

还可以在你的代码中使用一些预定义的模板函数,例如: 和 fmt.Printf 函数类似的 printf 函数:

示例 15.19—predefined_functions.go:

  1. package main
  2. import (
  3. "os"
  4. "text/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t = template.Must(t.Parse("{{with $x := `hello`}}{{printf `%s %s` $x `Mary`}} {{end}}!\n"))
  9. t.Execute(os.Stdout, nil)
  10. }
  11. // hello Mary!

15.6 章节 中也这样使用过:

  1. {{ printf "%s" .Body|html}}

否则 Body 的字节会被当做数字显示(字节默认都是 int8 类型的数字)