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.

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.

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).

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.

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 ... ?>.

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.

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

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}}
        ...
{{end}}

if Job is defined by


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:

{{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


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?

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.

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


{{. | 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.

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.

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

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

The signature for EmailExpander is typically

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


/**
 * 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


type Person struct {
        Name      string
        Emails     []string
}

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


{{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}}
{{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

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.

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


{"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:


{"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}}
  ]
}

which 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.

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.

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


{{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

{"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.

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:


/**
 * 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



Copyright © Jan Newmarch jan@newmarch.name Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.

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