Templates

模板

Many languages have mechanisms to convert strings from one form to another. Go has a template mechanism to convert strings based on the content of an object supplied as an argument. While this is often used in rewriting HTML to insert object values, it can be used in other situations. Note that this material doesn't have anything explicitly to do with networking, but may be useful to network programs.

很多编程语言都有字符串之间转换的机制,而GO语言则是通过模板来将一个对象的内容来作为参数传递从而字符串的转换。此方式不仅可以在重写HTML时插入对象值,也适用于其他方面。注意,本章内容并没有明确给出网络的工作方式,但对于网络编程方式很有用处。

Introduction

介绍

Most server-side languages have a mechanism for taking predominantly static pages and inserting a dynamically generated component, such as a list of items. Typical examples are scripts in Java Server Pages, PHP scripting and many others. Go has adopted a relatively simple scripting language in the template package.

大多数服务器端语言的机制主要是在静态页面插入一个动态生成的组件,如清单列表项目。典型的例子是在JSP、PHP和许多其他语言的脚本中。GO的template包中采取了相对简单的脚本化语言。

At the time of writing a new template package has been adopted. There is very little documentation on the template packages. There is a small amount on the old package, which is currently still available in the old/template. There is no documentation on the new package as yet apart from the reference page. The template package changed with r60 (released 2011/09/07).

因为新的template包是刚刚被采用的,所有现在的template包中的文档少的可怜,旧的old/template包中也还存有少量的旧模板。新发布的帮助页面还没有关于新包的文档。关于template包的更改请参阅r60 (released 2011/09/07).

We describe the new package here. The package is designed to take text as input and output different text, based on transforming the original text using the values of an object. Unlike JSP or similar, it is not restricted to HTML files but it is likely to find greatest use there.

在这里,我们描述了这个新包。该包是描述了通过使用对象值改变了原来文本的方式从而在输入和输出时获取不同的文本。与JSP或类似的不同,它的作用不仅限于HTML文件,但在那可能会有更大的作用。

The original source is called a template and will consist of text that is transmitted unchanged, and embedded commands which can act on and change text. The commands are delimited by {{ ... }} , similar to the JSP commands <%= ... =%> and PHPs <?php ... ?>.

源文件被称作 template ,包括文本传输方式不变,以嵌入命令可以作用于和更改文本。命令规定如 {{ ... }} ,类似于JSP命令 <%= ... =%> 和PHP命令 <?php ... ?>

Inserting object values

插入对象值

A template is applied to a Go object. Fields from that Go object can be inserted into the template, and you can 'dig" into the object to find subfields, etc. The current object is represented as '.', so that to insert the value of the current object as a string, you use {{.}}. The package uses the fmt package by default to work out the string used as inserted values.

模板应用于GO对象中.GO对象的字段被插入到模板后,你就能从域中“挖”到他的子域,等等。当前对象以'.'代替, 所以把当前对象当做字符串插入时,你可以采用{{.}}的方式。这个包默认采用 fmt 包来作为插入值的字符串输出。

To insert the value of a field of the current object, you use the field name prefixed by '.'. For example, if the object is of type

要插入当前对象的一个字段的值,你使用的字段名前加前缀 '.'。 例如, 如果要插入的对象的类型为


type Person struct {
        Name      string
        Age       int
        Emails     []string
        Jobs       []*Jobs
}

then you insert the values of Name and Age by

那么你要插入的字段 NameAge 如下


The name is {{.Name}}.
The age is {{.Age}}.

We can loop over the elements of an array or other list using the range command. So to access the contents of the Emails array we do

我们可以使用range命令来循环一个数组或者链表中的元素。所以要获取 Emails 数组的信息,我们可以这么干


{{range .Emails}}
        ...
{{end}}

if Job is defined by

如果Job定义为


type Job struct {
    Employer string
    Role     string
}

and we want to access the fields of a Person's Jobs, we can do it as above with a {{range .Jobs}}. An alternative is to switch the current object to the Jobs field. This is done using the {{with ...}} ... {{end}} construction, where now {{.}} is the Jobs field, which is an array:

如果我们想访问 Person字段中的 Jobs, 我们可以这么干 {{range .Jobs}}。这是一种可以将当前对象转化为Jobs 字段的方式. 通过 {{with ...}} ... {{end}} 这种方式, 那么{{.}} 就可以拿到Jobs 字段了,如下:


{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}

You can use this with any field, not just an array. Using templates

你可以用这种方法操作任何类型的字段,而不仅限于数组。亲,用模板吧!

Once we have a template, we can apply it to an object to generate a new string, using the object to fill in the template values. This is a two-step process which involves parsing the template and then applying it to an object. The result is output to a Writer, as in

当我们拥有了模板,我们将它应用在对象中生成一个字符串,用这个对象来填充这个模板的值。分两步来实现模块的转化和应用,并且输出一个Writer, 如下


t := template.New("Person template")
t, err := t.Parse(templ)
if err == nil {
        buff := bytes.NewBufferString("")
        t.Execute(buff, person)
}

An example program to apply a template to an object and print to standard output is

下面是一个例子来演示模块应用在对象上并且标准输入:


/**
 * PrintPerson
 */

package main

import (
        "fmt"
        "html/template"
        "os"
)

type Person struct {
        Name   string
        Age    int
        Emails []string
        Jobs   []*Job
}

type Job struct {
        Employer string
        Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}
        An email is {{.}}
{{end}}

{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}
`

func main() {
        job1 := Job{Employer: "Monash", Role: "Honorary"}
        job2 := Job{Employer: "Box Hill", Role: "Head of HE"}

        person := Person{
                Name:   "jan",
                Age:    50,
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
                Jobs:   []*Job{&job1, &job2},
        }

        t := template.New("Person template")
        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

The output from this is

输出如下:


The name is jan.
The age is 50.

        An email is jan@newmarch.name

        An email is jan.newmarch@gmail.com



    
        An employer is Monash
        and the role is Honorary
    
        An employer is Box Hill
        and the role is Head of HE
    

Note that there is plenty of whitespace as newlines in this printout. This is due to the whitespace we have in our template. If we wish to reduce this, eliminate newlines in the template as in

注意,上面有很多空白的输出,这是因为我们的模板中有很多空白。如果想消除它, 模板设置如下:


{{range .Emails}} An email is {{.}} {{end}}

In the example, we used a string in the program as the template. You can also load templates from a file using the function template.ParseFiles(). For some reason that I don't understand (and which wasn't required in earlier versions), the name assigned to the template must be the same as the basename of the first file in the list of files. Is this a bug?

在这个示例例中,我们用字符串应用于模板。你同样也可以用方法template.ParseFiles()来从文件中下载模板。因为某些原因,我还不没搞清楚(在早期版本没有强制要求),关联模板的名字必须要与文件列表的第一个文件的基名相同。话说,这个是BUG吗?

Pipelines

管道

The above transformations insert pieces of text into a template. Those pieces of text are essentially arbitrary, whatever the string values of the fields are. If we want them to appear as part of an HTML document (or other specialised form) then we will have to escape particular sequences of characters. For example, to display arbitrary text in an HTML document we have to change "<" to "&lt;". The Go templates have a number of builtin functions, and one of these is the function html. These functions act in a similar manner to Unix pipelines, reading from standard input and writing to standard output.

上述转换到模板中插入的文本块。这些字符基本上是任意的,是任何字符串的字段值。如果我们希望它们出现的是HTML文档(或其他的特殊形式)的一部分,那么我们将不得不脱离特定的字符序列。例如,要显示任意文本在HTML文档中,我们要将“<”改成“&lt”。GO模板有一些内建函数,其中之一是html。这些函数的作用与Unix的管道类似,从标准输入读取和写入到标准输出。

To take the value of the current object '.' and apply HTML escapes to it, you write a "pipeline" in the template

如果想用“.”来获取当前对象值并且应用于HTML转义,你可以在模板里写个“管道”:


{{. | html}}

and similarly for other functions.

其他方法类似。

Mike Samuel has pointed out a convenience function currently in the exp/template/html package. If all of the entries in a template need to be passed through the html template function, then the Go function Escape(t *template.Template) can take a template and add the html function to each node in the template that doesn't already have one. This will be useful for templates used for HTML documents and can form a pattern for similar function uses elsewhere.

Mike Samuel指出,目前在exp/template/html 包里有一个方便的方法。如果所有的模板中的条目需要通过html 模板函数,那么Go语言方法 Escape(t *template.Template)就能获取模板而后将html 函数添加到模板中不存在该函数的每个节点中。用于HTML文档的模板是非常有用的,并能在其他使用场合生成相似的方法模式。

Defining functions

定义方法

The templates use the string representation of an object to insert values, using the fmt package to convert the object to a string. Sometimes this isn't what is needed. For example, to avoid spammers getting hold of email addresses it is quite common to see the symbol '@' replaced by the word " at ", as in "jan at newmarch.name". If we want to use a template to display email addresses in that form, then we have to build a custom function to do this transformation.

模板使用对象化的字符串表示形式插入值,使用fmt包将对象转换为字符串。有时候,这并不是必需。例如,为了避免被垃圾邮件发送者掌握电子邮件地址,常见的方式是把字符号“@”替换为“at”,如“jan at newmarch.name”。如果我们要使用一个模板,显示在该表单中的电子邮件地址,那么我们就必须建立一个自定义的功能做这种转变。

Each template function has a name that is used in the templates themselves, and an associated Go function. These are linked by the type

每个模板函数中使用的模板本身有的一个名称,以及相关联的函数。他们用下面方式进行关联如下


type FuncMap map[string]interface{}

For example, if we want our template function to be "emailExpand" which is linked to the Go function EmailExpander then we add this to the functions in a template by

例如,如果我们希望我们的模板函数是“emailExpand”,用来关联到Go函数EmailExpander,然后,我们像这样添加函数到到模板中


t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

The signature for EmailExpander is typically

EmailExpander通常像这样标记:


func EmailExpander(args ...interface{}) string

In the use we are interested in, there should only be one argument to the function which will be a string. Existing functions in the Go template library have some initial code to handle non-conforming cases, so we just copy that. Then it is just simple string manipulation to change the format of the email address. A program is

我们感兴趣的是在使用过程中,那是一个只有一个参数的函数,并且是个字符串。在Go模板库的现有功能有初步的代码来处理不符合要求的情况,所以我们只需要复制。然后,它就能通过简单的字符串操作来改变格式的电子邮件地址。程序如


/**
 * PrintEmails
 */

package main

import (
        "fmt"
        "os"
        "strings"
        "text/template"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `The name is {{.Name}}.
{{range .Emails}}
        An email is "{{. | emailExpand}}"
{{end}}
`

func EmailExpander(args ...interface{}) string {
        ok := false
        var s string
        if len(args) == 1 {
                s, ok = args[0].(string)
        }
        if !ok {
                s = fmt.Sprint(args...)
        }

        // find the @ symbol
 substrs := strings.Split(s, "@")
        if len(substrs) != 2 {
                return s
        }
        // replace the @ by " at "
 return (substrs[0] + " at " + substrs[1])
}

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")

        // add our function
 t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

        t, err := t.Parse(templ)

        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

The output is

输出为:


The name is jan.

        An email is "jan at newmarch.name"

        An email is "jan.newmarch at gmail.com"

Variables

变量

The template package allows you to define and use variables. As motivation for this, consider how we might print each person's email address prefixed by their name. The type we use is again

template包,允许您定义和使用变量。这样做的动机,可能我们会考虑通过把他们的名字当做电子邮件地址前缀打印出来。我们又使用这个类型


type Person struct {
        Name      string
        Emails     []string
}

To access the email strings, we use a range statement such as

为了访问email的所有字符串, 可以用 range,如下


{{range .Emails}}
    {{.}}
{{end}}

But at that point we cannot access the Name field as '.' is now traversing the array elements and the Name is outside of this scope. The solution is to save the value of the Name field in a variable that can be accessed anywhere in its scope. Variables in templates are prefixed by '$'. So we write

但是需要指出的是,我们无法用'.' 的形式来访问字段 Name,因为当他被转化成数组元素时,字段Name并不包括其中。解决方法是,将字段Name 存储为一个变量,那么它就能在任意范围内被访问。变量在模板中用法是加前缀'$'。所以可以这样


{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}
{{end}}

The program is

程序如下:


/**
 * PrintNameEmails
 */

package main

import (
        "html/template"
        "os"
        "fmt"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}
{{end}}
`

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")
        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

with output

输出为


    Name is jan, email is jan@newmarch.name

    Name is jan, email is jan.newmarch@gmail.com

Conditional statements

条件语句

Continuing with our Person example, supposing we just want to print out the list of emails, without digging into it. We can do that with a template

继续我们那个Person的例子,假设我们只是想打印出来的邮件列表,而不关心其中的字段。我们可以用模板这么干


Name is {{.Name}}
Emails are {{.Emails}}

This will print


Name is jan
Emails are [jan@newmarch.name jan.newmarch@gmail.com]

because that is how the fmt package will display a list.

因为这个fmt包会显示一个列表。

In many circumstances that may be fine, if that is what you want. Let's consider a case where it is almost right but not quite. There is a JSON package to serialise objects, which we looked at in Chapter 4. This would produce

在许多情况下,这样做也没有问题,如果那是你想要的。让我们考虑下一种情况,它 几乎是对的但不是必须的。有一个JSON序列化对象的包,让我们看看第4章。它是这样的


{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com"]
}

The JSON package is the one you would use in practice, but let's see if we can produce JSON output using templates. We can do something similar just by the templates we have. This is almost right as a JSON serialiser:

JSON包是一个你会在实践中使用,但是让我们看看我们是否能够使用JSON输出模板。我们可以做一些我们有的类似的模板。这几乎就是一个JSON串行器:


{"Name": "{{.Name}}",
 "Emails": {{.Emails}}
}

It will produce

像这样组装


{"Name": "jan",
 "Emails": [jan@newmarch.name jan.newmarch@gmail.com]
}

which has two problems: the addresses aren't in quotes, and the list elements should be ',' separated.

其中有两个问题:地址没有在引号中,列表中的元素应该是','分隔。

How about this: looking at the array elements, putting them in quotes and adding commas?

这样如何:在数组中的元素,把它们放在引号中并用逗号分隔?


{"Name": {{.Name}},
  "Emails": [
   {{range .Emails}}
      "{{.}}",
   {{end}}
  ]
}

It will produce

像这样组装


{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com",]
}

(plus some white space.).

(再加上一些空白)。

Again, almost correct, but if you look carefully, you will see a trailing ',' after the last list element. According to the JSON syntax (see http://www.json.org/, this trailing ',' is not allowed. Implementations may vary in how they deal with this.

同样,这样貌似几乎是正确的,但如果你仔细看,你会看到尾有“,”在最后的列表元素。根据JSON的语法(请参阅 http://www.json.org/,这个结尾的','是不允许的。这样实现结果可能会有所不同。

What we want is "print every element followed by a ',' except for the last one." This is actually a bit hard to do, so a better way is "print every element preceded by a ',' except for the first one." (I got this tip from "brianb" at Stack Overflow.). This is easier, because the first element has index zero and many programming languages, including the Go template language, treat zero as Boolean false.

我们想要打印所有在后面带','的元素除了最后一个。"这个确实有点难搞, 一个好方法"在',' 之前打印所有元素除了第一个。" (我在 "brianb"的 Stack Overflow上提了建议)。这样更易于实现,因为第一个元素索引为0,很多编程语言包括GO模板都将0当做布尔型的false

One form of the conditional statement is {{if pipeline}} T1 {{else}} T0 {{end}}. We need the pipeline to be the index into the array of emails. Fortunately, a variation on the range statement gives us this. There are two forms which introduce variables

条件语句的一种形式是{{if pipeline}} T1 {{else}} T0 {{end}}。我们需要通过pipeline来获取电子邮件到数组的索引。幸运的是, range的变化语句为我们提供了这一点。有两种形式,引进变量


{{range $elmt := array}}
{{range $index, $elmt := array}}

So we set up a loop through the array, and if the index is false (0) we just print the element, otherwise print it preceded by a ','. The template is

所以我们遍历数组,如果该索引是false(0),我们只是打印的这个索引的元素,否则打印它前面是','的元素。模板是这样的


{"Name": "{{.Name}}",
 "Emails": [
 {{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
 {{end}}
 ]
}

and the full program is

完整的程序如下


/**
 * PrintJSONEmails
 */

package main

import (
        "html/template"
        "os"
        "fmt"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `{"Name": "{{.Name}}",
 "Emails": [
{{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
{{end}}
 ]
}
`

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")
        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

This gives the correct JSON output.

上面给出的是正确的JSON输出

Before leaving this section, we note that the problem of formatting a list with comma separators can be approached by defining suitable functions in Go that are made available as template functions. To re-use a well known saying, "There's more than one way to do it!". The following program was sent to me by Roger Peppe:

在结束本节之前,我们强调了用逗号分隔的列表格式的问题,解决方式是可以在模板函数中定义适当的函数。正如俗话说的,“道路不止一条!”下面的程序是Roger Peppe给我的:


/**
 * Sequence.go
 * Copyright Roger Peppe
 */

package main

import (
        "errors"
        "fmt"
        "os"
        "text/template"
)

var tmpl = `{{$comma := sequence "" ", "}}
{{range $}}{{$comma.Next}}{{.}}{{end}}
{{$comma := sequence "" ", "}}
{{$colour := cycle "black" "white" "red"}}
{{range $}}{{$comma.Next}}{{.}} in {{$colour.Next}}{{end}}
`

var fmap = template.FuncMap{
        "sequence": sequenceFunc,
        "cycle":    cycleFunc,
}

func main() {
        t, err := template.New("").Funcs(fmap).Parse(tmpl)
        if err != nil {
                fmt.Printf("parse error: %v\n", err)
                return
        }
        err = t.Execute(os.Stdout, []string{"a", "b", "c", "d", "e", "f"})
        if err != nil {
                fmt.Printf("exec error: %v\n", err)
        }
}

type generator struct {
        ss []string
        i  int
        f  func(s []string, i int) string
}

func (seq *generator) Next() string {
        s := seq.f(seq.ss, seq.i)
        seq.i++
        return s
}

func sequenceGen(ss []string, i int) string {
        if i >= len(ss) {
                return ss[len(ss)-1]
        }
        return ss[i]
}

func cycleGen(ss []string, i int) string {
        return ss[i%len(ss)]
}

func sequenceFunc(ss ...string) (*generator, error) {
        if len(ss) == 0 {
                return nil, errors.New("sequence must have at least one element")
        }
        return &generator{ss, 0, sequenceGen}, nil
}

func cycleFunc(ss ...string) (*generator, error) {
        if len(ss) == 0 {
                return nil, errors.New("cycle must have at least one element")
        }
        return &generator{ss, 0, cycleGen}, nil
}

Conclusion

结论

The Go template package is useful for certain kinds of text transformations involving inserting values of objects. It does not have the power of, say, regular expressions, but is faster and in many cases will be easier to use than regular expressions

template包在对于某些类型的文本转换涉及插入对象值的情况是非常有用的。虽然它没有正则表达式功能强大,但它执行比正则表达式速度更快,在许多情况下比正则表达式更容易使用。


Copyright © Jan Newmarch, jan@newmarch.name

If you like this book, please contribute using Flattr
or donate using PayPal