使用go模板

原文链接:[Gopher Academy Blog][https://blog.gopheracademy.com/advent-2017/using-go-templates/] Marko Mudrinić

go模板是自定义输出的强大方法,无论你是想创建页面,发送邮件,还是与Buffalo, Go-Hugo,集成,或者仅仅是使用CLI,如kubectl.

golang中有两个包用于模板操作:text/templatehtml/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 . elseelse 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

这里有三个最重要,使用最频繁的函数:

下面的代码展示了上面提到的三种函数在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:templatetemplate-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等工具。