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.
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 ... ?>
.
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?
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 "<". 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.
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"
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
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
This will print
Name is {{.Name}}
Emails are {{.Emails}}
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
}
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
If you like this book, please contribute using Flattr
or donate using PayPal