Dynamic Templates in Go

Share

We've recently written a couple of lambda functions in Go here at Crimson Macaw and had the chance to get hands on with dynamic templates. For those with a background in coding templates found in other programming languages e.g. Python, the Go templates with the package html/template will work in a similar way but have a slightly different format.

Simple Templates

Let's say we've got the following Go email template, as a string in our code:

var input = `<p>{{.character}} has travelled from year {{.origin_year}} to year {{.destination_year}}</p>

	<p>When approaching the singularity, it has come to our attention that an anomaly has occurred in Winden when {{.character}} has travelled from {{.origin_year}} to {{.destination_year}}</p>

	<pre>We believe this will have repercussions on {{.effect}}</pre>

	<p>Regards,<br/>Dark Universe Data Lake, managed by Crimson Macaw Wormhole</p>`

We can see that Go uses double braces {{}} to distinguish which structures of the template are going to be evaluated. Structures can be values, in which case a dot is used to prefix the field e.g. .origin_year, or functions, in which case no prefix is used e.g. length (see below).

We can store all values to be referenced by the template in the data map, of type map[string]interface{}, that we can initialise as:

data := make(map[string]interface{}, 4)
data["character"] = "Jonas Kahnwald"
data["origin_year"] = 2019
data["destination_year"] = 2052
data["effect"] = "the world stability after the Apocalypse"

The next step is to use the go html/template package to create a function, AddSimpleTemplate, that will replace the fields in our template with elements in our data map. We will have a string, which is where our template is saved, and a map[string]interface{} i.e. our data map as inputs to this function. Inside the function, Parse reads the template from the string while Execute evaluates its dynamic structures. The result is put in a buffer that is then converted into a string and returned. Alternatively, we can store the template in a file and use Parsefile to load it.

func AddSimpleTemplate(a string,b map[string]interface{}) string {
	tmpl := template.Must(template.New("email.tmpl").Parse(a))
	buf := &bytes.Buffer{}
        err := tmpl.Execute(buf, b)
	if err != nil {
		panic(err)
        }
	s := buf.String()
	return s
}

In the main function, we can call and print the results of this function,

fmt.Println(AddSimpleTemplate(input,data))

and the output will look like:

output-template-1

More Complex Templates

We have now seen how we can use html/template in Go to dynamically replace fields in our template but what if we have got logic in the template i.e. for loops and if statements? What if we've got another map within our data map that we want to look through and do checks against its elements? And what if this map needs to be dynamic i.e. have different elements every time we read it?

All these three requirements are met by having a Go template which includes logic between fields inside the curly braces as well as using type map[string]interface{} to store any dynamic maps within our main map.

Logic in Go Html Templates

For loops are executed via range and if statements via if if the condition is true. Both can also use else with a condition that is false. Inside the if statements, a series of operators can be used:

  • eq for ==
  • ne for !=
  • lt for <
  • le for <=
  • gt for >
  • ge for >=

More information on the logic can be found here.

Templates with Logic Example

We've got the following Go html template, stored in a string variable, input2, that includes a for loop, an if else statement and an if statement. It also references a function in the code called length, that calculates the length of a map.

var input2 = `<p>Series of anomalies found in {{.country}}, {{.city}}</p>
	  <p>Hello</p>
		<p>This is an automated notification in response to a global time travel event. The
		<b>singularity in {{.country}}, {{.city}}</b> detected anomalies at <b>{{.lastModified}}</b>.</p>
		<p>Singularity power source: {{.source}}</p>
		<p>Supplier: {{.supplier}}</p>
		<p>Portal: {{.portal}}</p>
		<p>A total of <b>{{.totalAnomalies}}</b> anomalies have been found:</p>
		<ul>
		{{- range .anomalies -}}
		{{if .character -}}
		<li>Anomaly {{.record}}: {{.character}} has travelled from {{.origin_year}} to {{.destination_year}}</li>
		{{- else }}
		  <li>Anomaly {{.record}} - {{.message}}</li>
		{{- end}}
	  {{- end}}
	  </ul>
	  {{if gt .totalAnomalies (length .anomalies) -}}
		<p><i><b>Note:</b> only up to the first {{length .anomalies}} anomalies have been displayed, a total of {{.totalAnomalies}} anomalies have been identified.</i></p>
		{{- end}}
	  <p>Regards,<br/>Dark Universe Data Lake, managed by Crimson Macaw Wormhole</p>`

func Length(a interface{}) float64 {
	 	return float64(reflect.ValueOf(a).Len())
}

Here we loop over the records of the anomalies map and, if a record contains a character, a certain message will be printed, whereas if the record does not contain a character, another message is printed. Once the loop has finished, we check the length of the anomalies map, which is computed via length .anomalies, compare it with the field totalAnomalies and, if the latter is greater, a note is printed.

We are going to slightly modify our AddSimpleTemplate and save it as a new function, AddTemplate, by including outer functions that, if found in the template, are called by the html/template package when evaluating the dynamic structures from the template. To do this, before parsing and executing the template, we call FuncMap to map the function that calculates the length of the anomalies map i.e. Length to the function name in the template i.e. length.

func AddTemplate(a string,b map[string]interface{}) string {
       fmap := template.FuncMap{
		"length": Length,
	}
	tmpl := template.Must(template.New("email.tmpl").Funcs(fmap).Parse(a))
	buf := &bytes.Buffer{}
        err := tmpl.Execute(buf, b)
	if err != nil {
		panic(err)
        }
	s := buf.String()
	return s
}

Looking at the fields in the template, it looks like this time our input map is going to contain a few strings e.g. country, a timestamp i.e. lastModified, and a map, anomalies, that we are going to iterate through. It is therefore going to be much bigger than our initial data map, which we initialised in the main function. As a result, the easier option is to have our input in json format, which we're then going to unmarshal in a map. For instance, we can have a file event.json that looks like this:

{
	"country": "Germany",
	"city": "Winden",
	"lastModified": "2020-05-09T15:17:51.487Z",
	"source": "dark matter",
	"supplier": "Massive Dynamic",
	"portal": "cave system beneath the nuclear plant",
	"totalAnomalies":20,
	"anomalies": [
		{
			"record":1,
			"character":"Mikkel Nielsen",
			"origin_year":2019,
			"destination_year":1986
		},
		{
			"record":1,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		},
		{
			"record":2,
			"character":"Jonas Kahnwald",
			"origin_year":2019,
			"destination_year":2053
		},
		{
			"record":2,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		},
		{
			"record":3,
			"character":"Jonas Kahnwald",
			"origin_year":2053,
			"destination_year":1921
		},
		{
			"record":3,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		},
		{
			"record":4,
			"character":"Hannah Kahnwald",
			"origin_year":2020,
			"destination_year":1954
		},
		{
			"record":4,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		},
		{
			"record":5,
			"character":"Ulrich Nielsen",
			"origin_year":2019,
			"destination_year":1953
		},
		{
			"record":5,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		},
		{
			"record":6,
			"character":"Claudia Tiedemann",
			"origin_year":1986,
			"destination_year":2019
		},
		{
			"record":6,
			"message":"Attention! This might have disastrous consequences on the timeline!"
		}
	]
}

In the main function, we then read event.json using os.Stdin, unmarshal its contents into the event map, and call the AddTemplate function.

var event map[string]interface{}
	 dat, err := ioutil.ReadAll(os.Stdin)
	 err = json.Unmarshal(dat, &event)
	 if err !=nil {
		 panic(err)
	 }

fmt.Println(AddTemplate(input2,event))

The output will look like:

output-template-2

NB to read a local file from Stdin at runtime, we can can execute cat event.json | go run template.go, if our code is saved as template.go and our local file as event.json.

The entire code can be found below.

package main

import (
	"html/template"
	"reflect"
	"bytes"
	"fmt"
	"io/ioutil"
	"encoding/json"
	"os"
)

func Length(a interface{}) float64 {
	return float64(reflect.ValueOf(a).Len())
}

func AddTemplate(a string,b map[string]interface{}) string {
	fmap := template.FuncMap{
		"length": Length,
	}
	tmpl := template.Must(template.New("email.tmpl").Funcs(fmap).Parse(a))
	buf := &bytes.Buffer{}
        err := tmpl.Execute(buf, b)
	if err != nil {
  	panic(err)
        }
	s := buf.String()
	return s
}

func AddSimpleTemplate(a string,b map[string]interface{}) string {
	tmpl := template.Must(template.New("email.tmpl").Parse(a))
	buf := &bytes.Buffer{}
        err := tmpl.Execute(buf, b)
	if err != nil {
		panic(err)
        }
	s := buf.String()
	return s
}

func main() {
	data:=make(map[string]interface{}, 4)
	data["character"]="Jonas Kahnwald"
	data["origin_year"]=2019
	data["destination_year"]=2052
	data["effect"]="the world stability after the Apocalypse"

	var input = `<p>{{.character}} has travelled from year {{.origin_year}} to year {{.destination_year}}</p>

	<p>When approaching the singularity, it has come to our attention that an anomaly has occurred in Winden when {{.character}} has travelled from {{.origin_year}} to {{.destination_year}}</p>

	<pre>We believe this will have repercussions on {{.effect}}</pre>

	<p>Regards,<br/>Dark Universe Data Lake, managed by Crimson Macaw Wormhole</p>`

	fmt.Println(AddSimpleTemplate(input,data))

	var input2 = `<p>Series of anomalies found in {{.country}}, {{.city}}</p>
	  <p>Hello</p>
		<p>This is an automated notification in response to a global time travel event. The
		<b>singularity in {{.country}}, {{.city}}</b> detected anomalies at <b>{{.lastModified}}</b>.</p>
		<p>Singularity power source: {{.source}}</p>
		<p>Supplier: {{.supplier}}</p>
		<p>Portal: {{.portal}}</p>
		<p>A total of <b>{{.totalAnomalies}}</b> anomalies have been found:</p>
		<ul>
		{{- range .anomalies -}}
		{{if .character -}}
		<li>Anomaly {{.record}}: {{.character}} has travelled from {{.origin_year}} to {{.destination_year}}</li>
		{{- else }}
		  <li>Anomaly {{.record}} - {{.message}}</li>
		{{- end}}
	  {{- end}}
	  </ul>
	  {{if gt .totalAnomalies (length .anomalies) -}}
		<p><i><b>Note:</b> only up to the first {{length .anomalies}} anomalies have been displayed, a total of {{.totalAnomalies}} anomalies have been identified.</i></p>
		{{- end}}
	  <p>Regards,<br/>Dark Universe Data Lake, managed by Crimson Macaw Wormhole</p>`

  	var event map[string]interface{}
        dat, err := ioutil.ReadAll(os.Stdin)
        err = json.Unmarshal(dat, &event)
        if err !=nil {
	  panic(err)
        }

        fmt.Println(AddTemplate(input2,event))

}