Examples of consuming data in Go templates

While working on creating a template file for a Go project, I wanted to better understand how to work with data in Go templates as available via the html/template package. In this post, I discuss a few use cases that may arise.

Accessing a variable

Let’s consider our first program:

package main

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

func main() {

	var names = []string{"Tabby", "Jill"}

	tmpl := template.New("test")

	tmpl, err := tmpl.Parse("Array contents: {{.}}")
	if err != nil {
		log.Fatal("Error Parsing template: ", err)
		return
	}
	err1 := tmpl.Execute(os.Stdout, names)
	if err1 != nil {
		log.Fatal("Error executing template: ", err1)

	}
}

When we run the above program Go playground link, the output we get is:

Array contents: [Tabby Jill]

There are three main stages to working with templates in general that we see in the program above:

Anything within {{ }} inside the template string is where we do something with the data that we pass in when executing the template. This something can be just displaying the data, or performing certain operations with it.

The . (dot) refers to the data that is passed in. In the above example, the entire array contents of names is the value of .. Hence, the output has the entire array including the surrounding []. This also means that names could have been of another type - a struct for example like so:

..

type Test struct {
	name string
}

func main() {

	..

	//parse some content and generate a template
	tmpl, err := tmpl.Parse("Variable contents: {{.}}")
	if err != nil {
		log.Fatal("Error Parsing template: ", err)
		return
	}
	..
}

The output now would be:

Variable contents: {Tabby}

Accessing structure members

Now, let’s consider that our structure has multiple members and we want to access the individual members in our template. Here’s how we can do so (Golang playground)[https://play.golang.org/p/8BSiYJ_7Mfd]:

package main

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

type Person struct {
	Name string
	Age  int
}

func main() {

	p := Person{Name: "Tabby", Age: 21}

	tmpl := template.New("test")

	//parse some content and generate a template
	tmpl, err := tmpl.Parse("{{.Name}} is {{.Age}} years old")
	if err != nil {
		log.Fatal("Error Parsing template: ", err)
		return
	}
	err1 := tmpl.Execute(os.Stdout, p)
	if err1 != nil {
		log.Fatal("Error executing template: ", err1)

	}
}

The dot operator referes to the structure object, p and then inside the template, we just specify the field name, like so, .<Field>. The output will be:

Tabby is 21 years old

Do something with array elements

Going back to our first example, how do we access the individual array elements? Let’s see how we can do so.

The complete example can be found here, but the only change is in the template string:

tmpl, err := tmpl.Parse("{{range .}}Hello {{.}}\n{{end}} ")

I find it easy when I read the above template string as:

for _, item := range names {       // corresponding to {{range .}}
    fmt.Printf("Hello %s\n", item) // corresponding to Hello {{.}}\n
}                                 // corresponding to {{end}}

range can be used to iterate over arrays, slice, map or a channel.

Arrays of structure objects

Combining the two previous examples, we can access array elements which are structure objects, like so:

...
var names = []Person{
		Person{Name: "Tabby", Age: 21},
		Person{Name: "Jill", Age: 19},
}

tmpl := template.New("test")

tmpl, err := tmpl.Parse("{{range .}}{{.Name}} is {{.Age}} years old\n{{end}} ")
err1 := tmpl.Execute(os.Stdout, names)
...

The complete program is here and the output from this program is:

Tabby is 21 years old
Jill is 19 years old

Calling user defined functions and Chaining

Our next example demonstrates two new things:

The complete example is available here and the output is:

Tabby has an odd name 
Jill has an even name 

The two main changes from our previous program are:

Adding a FuncMap

funcMap := template.FuncMap{  
    "oddOrEven": oddOrEven,
}

tmpl := template.New("test").Funcs(funcMap)

A FuncMap is how we add our functions to a template’s “context” and then invoke them. There are few rules around the semantics of functions we can add which you can learn here. My favorite is if I return a non-nil error, the template execution will halt without me having to do any extra checks.

Chaining

Chaining is how we perform an action and feed it’s output to another action via the | (pipe) operator:

tmpl, err := tmpl.Parse("{{range .}}{{.Name}} has an {{len .Name | oddOrEven}} name \n{{end}}")

Here, we invoke the in-built len function to calculate the length of Name and then call the oddOrEven function.

Controlling output using template strings

My first encounter with Go templates was when working with docker output formatting which allowed controlling what I get as output. Let’s see how we can implement something like that for our program. The entire program is here.

When we run it without passing any arguments:

$ go run test.go
Tabby 21 odd
Jill 19 even

If however we pass it a format string as the first command line argument, we can control the output:

$ go run test.go "{{ .Age }} {{ OddOrEven .Name}}"
21 odd
19 even

The two main changes are:

func OddOrEven(s string) string {

        if len(s)%2 == 0 {
                return "even"
        } else {
                return "odd"
        }

}

The format string is now obtained via a function call:

func getFormatString() string {
        placeHolderFormat := "{{range .}}%s\n{{end}}"
        defaultFormatString := "{{.Name}} {{.Age}} {{ OddOrEven .Name}}"
        if len(os.Args) == 2 {
                return fmt.Sprintf(placeHolderFormat, os.Args[1])
        } else {
                return fmt.Sprintf(placeHolderFormat, defaultFormatString)
        }
}

We can of course define any arbitrary functions and make them available to be invoked in the context of our templates.

Rendering an arbitrary template file using arbitrary values

Our next program will take a template string in a file, like so:

$ cat cluster.tmpl
Cluster Name: {{.clusterName}}
Max Nodes: {{.maxNodes}}
Nodes: {{range .nodeNames}}
- {{.}}
{{- end}}

The data will be provided as an YAML file, like so:

$ cat values.yml
clusterName: "test.local"
maxNodes: 10
nodeNames:
- Node 1
- Node 2

And our program will print:

Cluster Name: test.local
Max Nodes: 10
Nodes:
- Node 1
- Node 2

We will take advantage of a third-party package ghodss/yaml to parse our YAML file and the complete program is here.

The key bit in the program was to create a map of type [string]interface from the provided YAML file. We will run the program as:

$ go run main.go cluster.tmpl values.yml
Cluster Name: test.local
Max Nodes: 10
Nodes:
- Node 1
- Node 2

As a side note, note the dash in {{- end}}? That is to prevent newlines and spaces. I still don’t quite get it, but it seems like a hit and trial thing!

Accessing a map object

Complete example here. You will see that by default range . iterates over the map’s values, rather than keys (opposite of what we see in the Go language).

Explore

There’s a lot more to explore in Golang templates. Check out: