By Andreea Dogaru – BI Engineer
We’ve recently written a couple of lambda functions in Go here at synvert TCM (formerly Crimson Macaw). Meaning we 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
evaluating its dynamic structures. We then place the result in a buffer which converts to a string and returns. 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:
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 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. Alongside this, we can use 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
is!=
lt
for<
le
is<=
gt
for>
ge
is>=
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. If a record contains a character, a certain message will show, whereas if the record does not contain a character, another message shows. Once the loop finishes, we check the length of the anomalies
map, which is computed via length .anomalies
. We then compare it with the field totalAnomalies
and, if the latter is greater, a note will show.
Modify
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. We’re then going to unmarshal on 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:
NB to read a local file from Stdin at runtime, we can execute cat event.json | go run template.go
, if our code is saved as template.go and our local file as event.json.
Check out the full code 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))
}
Want to see more of our work? Check out our blogs here.