使用go模板
原文链接:[Gopher Academy Blog][https://blog.gopheracademy.com/advent-2017/using-go-templates/] Marko Mudrinić
go模板是自定义输出的强大方法,无论你是想创建页面,发送邮件,还是与Buffalo, Go-Hugo,集成,或者仅仅是使用CLI,如kubectl.
golang中有两个包用于模板操作:text/template和html/templates.两个包提供了相同的接口,但html/templates用于生成HTML输出,代码代码注入安全。
本文将速快浏览如何使用模板以及如何在你的应用中集成模板。
Actions
在学习如何实现之前,先看看template的语法。一些合适的函数以字符串或”原生字符串”的形式提供给模板。Actions代表data evaluations, functions或controll loops。
Data evaluations
通常在使用模板的时候,需要将模板绑定到其获取数据的go的数据结构上(如struct)。如果从结构体struct中获取数据,你需要使用action,这个位置会在解析的时候使用给定结构体的`FieldName`的值替换。结构体struct需要传给`Execute`函数。
还有一种 action, 使用它你可以引用一个非结构体类型的值。
Conditions
你可以在模板中使用if条件,比如i想检查FieldName是否非空。如果不为空,就打印它的值: Value of FieldName is  .
else和else if还支持如下模式:
 // action  // action 2 .
Loops
使用range action可以遍历一个slice, range action格式如下:
 ... 
如果slice的类型不是结构体,你可以使用 action来引用这个值。如果slice的类型是结构体,你可以使用 action来引用其值。
Functions, Pipelines and Variables
一些Action有一些内置函数可以结合管道来对输出进一步的解析。管道使用|符号,默认行为是把左侧的数据发送给管道右侧的函数。
函数用于转义action的结果。有一些默认的函数如html用于返回转义HTML后的输出,防止代码注入;js函数返回JavaScript转义后的输出。
使用with action,你可以定定义变量,变量的作用范围在with语句块内有效:
  .
本文通篇会包含一些复杂的action,比如从array中而不是struct中读取数据。
Parsing Templates
这里有三个最重要,使用最频繁的函数:
- New: allocates new, undefined template
 - Parse: parses given template string and return parsed template
 - Execute: applies parsed template to the data structure and write result to the given writer
 
下面的代码展示了上面提到的三种函数在action中的用法:
package main
import (
	"os"
	"text/template"
)
type Todo struct {
	Name        string
	Description string
}
func main() {
	td := Todo{"Test templates", "Let's test a template to see the magic."}
  t, err := template.New("todos").Parse("You have a task named \"\" with description: \"\"")
	if err != nil {
		panic(err)
	}
	err = t.Execute(os.Stdout, td)
	if err != nil {
		panic(err)
	}
}
执行上面的代码,会在终端输出如下内容:
You have a task named "Test templates" with description: "Let's test a template to see the magic."
你可以复用模板,而不需要重新创建和解析,只需要提供Execute函数所需要的结构体数据。
// code omitted beacuse of brevity
...
tdNew := Todo{"Go", "Contribute to any Go project"}
err = t.Execute(os.Stdout, tdNew)
}
如你所见,模板提供了自定义文本输出的强大功能。除了控制文本输出,还可以通过html/templates控制html的输出。
Verifying Templates
由于模板通常是在编译期间就固定下来的,如果模板无法解析,这将是程序一个严重的bug。template包提供了Must函数,用于验证在模板解析过程中是否有效。Must函数与上面手动检查错误的结果相同。这可以节省代码输入量。当有错误是,应用会发生panic。如果需要进一步处理error,使用上面手动的方式比使用Must函数更加容易。
Must函数需要接受一个template和error 作为参数,通常是将New函数作为其参数:
t := template.Must(template.New("todos").Parse("You have task named \"\" with description: \"\""))
本文会采用这个函数,而不使用显示的错误检查的方法。
Implementing Templates
这里将展示如何使用template魔法特性。首先创建一个简单的包含to-do列表的页面。
Creating Web Pages using Templates
html/tempaltes包允许提供模板文件,如html格式的文件,这样使得前后端的实现更加容易。下面的数据结构代表一个To-Do列表,这个列表是一个stuct类型的slice,包含任务的名称和状态。
type entry struct {
  Name string
  Done bool
}
type ToDo struct {
  User string
  List []entry
}
一个简单的html页面用来展示用户的名称和它的To-Do列表。本例中将使用range action来loop through tasks slice, 使用with从slice中取数据,还包含一个条件检查,如果任务已经完成的话。任务完成的情况下,Yes会被写入结构体成员,否则no写入。
<!DOCTYPE html>
<html>
  <head>
    <title>Go To-Do list</title>
  </head>
  <body>
    <p>
      To-Do list for user:  
    </p>
    <table>
      	<tr>
          <td>Task</td>
          <td>Done</td>
    	</tr>
      	
			
      			<tr>
              		<td></td>
              		<td>YesNo</td>
      			</tr>
			 
      	
    </table>
  </body>
</html>
和前面一样,我们会解析模板并将其应用于struct的数据,这里不使用Parse函数,而是使用ParseFile函数。为了代码简短,我们将解析后的数据打到标准输出,而不是HTTP 的Writer接口。
package main
import (
	"html/template"
	"os"
)
type entry struct {
	Name string
	Done bool
}
type ToDo struct {
	User string
	List []entry
}
func main() {
	// Parse data -- omitted for brevity
	// Files are provided as a slice of strings.
	paths := []string{
		"todo.tmpl",
	}
    t := template.Must(template.New("html-tmpl").ParseFiles(paths...))
	err = t.Execute(os.Stdout, todos)
	if err != nil {
		panic(err)
	}
}
代码执行结果如下:
<!DOCTYPE html>
<html>
  <head>
    <title>Go To-Do list</title>
  </head>
  <body>
    <p>
      To-Do list for user: gopher 
    </p>
    <table>
      	<tr>
          <td>Task</td>
          <td>Done</td>
    	</tr>
      			<tr>
              		<td>GopherAcademy Article</td>
              		<td>Yes</td>
      			</tr>			
      			<tr>
              		<td>Merge PRs</td>
              		<td>No</td>
      			</tr>
    </table>
  </body>
</html>
Parsing Multiple Files
有时你有多个模板文件,或者你想动态的增减模板文件。这种场景下可以使用ParseGlob函数,此接受glob作为参数,并解析所有匹配glob的文件:
// ...
t := template.Must(template.New("html-tmpl").ParseGlob("*.tmpl"))
err = t.Execute(os.Stdout, todos)
if err != nil {
	panic(err)
}
// ...
Customizing Command’s Output
你也可以在CLI中使用模板,这样用户就可以自定义命令的输出。下面的代码片段通过两个flag:template和template-file来解析指定的模板。
package main
import (
	"flag"
	"os"
)
func main() {
  	// data parsing...
  
	var template, templateFile string
	flag.StringVar(&template, "template", "", "a template")
	flag.StringVar(&templateFile, "template-file", "", "a template file path")
	flag.Parse()
	if templateFile != "" {
		path := []string{templateFile}
		t := template.Must(template.New("html-tmpl").ParseFiles(path...))
		err = t.Execute(os.Stdout, todos)
		if err != nil {
			panic(err)
		}
	} else if template != "" {
		path := []string{templateFile}
		t := template.Must(template.New("html-tmpl").Parse(template))
		err = t.Execute(os.Stdout, todos)
		if err != nil {
			panic(err)
		}
	} else {
		// non-template data logic...
	}
}
相似功能也可以通过spf13/cobra包实现,处于篇幅的考虑,这里的代码省略的数据解析的逻辑。通过这种方式可以自定义更加直观的输出,而不需要使用sed,awk或grep等工具。