Templates

Introduction

许多服务器端的语言都可以在静态页面中插入动态生成的组件,比如一个项目列表。常见的有JSP, PHP等等。Go在template包中,采用了一个相对简单的角本语言。

这个包设计目的是接收一个文本,输出不同的文本。这基于使用的对像的值,来改变原始文本。

original source被称为template, 构成模板的文本不会被改变。而嵌入的命令将起作用,而且改变文本。命令是通过{{ ... }}声明,这跟JSP的命令<%= ... =%>一样。

Inserting object values

一个模板需要应用一个Go对像。Go对像的字段可以被插入到模板,你可以深入到对像的子字段。当前的对像以'.'表示。所以要将当前对像的值作为字符串插入,你可以使用{{.}}. template默认使用fmt输出字符串和插入值。

为了插入当前对像字段的值,你可以使用字段的名字,前缀加上'.'. 举例来说,如果object是一个类型。

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

则你可以插入Name和Age

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

你也可以能过range命令,遍历元素。所以它可以访问数组Emails的内容

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

如果Job通过以下方式定义

type Job struct {
    Employer string
    Role string
}

如果我们要访问Person中的Jobs字段,我们可以使用{{range .Jobs}}. 另一个方法是将当前对像转换为Jobs字段。 这通过使用{{with ...}} ... {{end}}结构. 这样{{.}}表示的是Jobs的字段,它是一个数组

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

对于任何字段,都可以使用这种方式,而不仅仅是数组

使用templates

一旦你有一个template, 我们可以向它应用一个对像,产生一个新的字符串,使用一个对像填充template的值。这由两个过程,首先解析一个模板,然后向它应用一个对像。结果输出到一个Writer. 如下所示

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

以下的例子中,程序向你一模板应用了一个对像,并且打印到标准输出端


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 roles 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{"[email protected]", "[email protected]"},
        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)
    }
}

注意,这里会有大量的空格输出。这是因为我们在template中有很多空白。如果我们想减小它,可以在template中使用

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

在这个例子中,我们使用了一个字符串,作为模板。你可以使用template.ParseFiles()函数加载外部的文件。

Pipelines

上面是向模板中插入几块要转换的文本。这些文本可以是任意的。可以是任何字段的字符串值。如果我们想这些值以HTML文档显示,则我们需要对字符进行转义。比如,"<"转换为"& lt;"。 在Go templates中有许多内置的函数,其中一个是html. 这个函数类型于Unix的pipeline, 从标准输入端读取,然后输出到标准输出端。

以下是它的用法

{{. | html}}

这同样适用于其它函数

Mike Samuel指出,在exp/template/html包中有更加方便的函数。如果template中所有的entries实体,都需要传递给html, 则可以使用Go函数Escape(t *template.Template),它接收一个模板,并且给模板中的每一个节点没有html, 则添加html. 这对于html文档来说非常有用。

Defining functions

模板使用一个对像的字段的字符串值来插入值。使用fmt包将一个对像转换为字符串。有的时候并不需要使用到fmt. 比如,为了避免垃级邮件获得邮箱地址,可以使用at代替'@'. 即"jan at newmarch.name". 如果我们想使用一个模板来以这种方式来显示邮件。则我们要创建一个自定义的函数来做这个转换。

在模板中,每一个函数都有一个名字. 它通过以下方式,将Go中的函数,分配给模板

type FuncMap map[string]interface{}

举个例子来说,我们的模板函数为"emailExpand",它连接到一个Go函数EmailExpander,则我们通过以下方式,将这个函数添加为模板函数。

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

EmailExpander的签名通常为

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

通常这里的参数数量是一个。在Go的template library中有很多函数。我们可以从复制这些函数,在进行修改。

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{"[email protected]", "[email protected]"},
    }
    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)
    }
}

Output

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

变量

template包允许你定义和使用变量,比如,我们打印的每个人的邮件地址中,都是它们的名字。

type Person struct {
    Name string
    Emails []string
}

为了获得email字符串,我们可以使用range语句,如下

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

但在这里机,我们不能访问Name字段,'.'已经表示的是数组元素。而Name是在外部作用域。一种解决办法是将Name的值保存到一个变量中。这样就可以在任何作域名内访问。变量在模板中会添加一个'$'作为前缀。

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

条件语句

继续我们Person的例子, 假设我们仅仅想输出emails列表,而不使用range. 则我们可以使用

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

将会打印出

Name is jan
Emails are [[email protected] [email protected]]

这是由fmt格式显示。

这对于很多情况下是我们想要的,但并不是所有情况。比如JSON包序列化对像。它要产生以下的格式

{"Name": "jan",
"Emails": ["[email protected]", "[email protected]"]
}

在实际中我们可以使用JSON包来生成上面的字符串。但我们也可以使用template来生成。

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

它将生成

{"Name": "jan",
"Emails": [[email protected] [email protected]]
}

但这有两个问题:邮件地址没有被双引号引用,每一个元不比没有被','分隔。

那我们应该怎么做:遍历每个元素,然后添加引号和逗号吗?

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

Output

{"Name": "jan",
"Emails": ["[email protected]", "[email protected]",]
}

但在最后一个元素中也会添加上逗号。依据JSON语法,http://www.json.org. 这是不允许的

我们想要打印每个元素,并且在每一个元素后添加一个',',除了最后的一个。在实际中,这很难做到,所以更好的办法是在打印每个元素的之前添加','. 最非常简单,因为第一个元素的索引是0. 而在许多语言中,包括Go template language. 都是将0视为false.

Go template中的条件语句为{{if pipeline}} T1 {{else}} T0 {{end}}. 我们需要pipeline为数组emails的索引。幸运的是,range可以为我们完成。如下所示

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

所以我们上面的条件语句,可以写为

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

整个的程序为

/**
* 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{"[email protected]", "[email protected]"},
    }
    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)
    }
}

在我们离开本节之前,我们可以定义一个模板函数来处理逗号分隔。

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
}

Output

a, b, c, d, e, f
a in black, b in white, c in red, d in black, e in white, f in red