Example Of Using Templates In Golang

Almost every programming language has a library implementing templating. In epoch of server side MVC dominance, templating was so important that it could determine language success or failure. Nowadays, however, when single page applications get momentum, templates are used only occasionally.

The niche where templates are still absolutely indispensable is email content generation. In this post I will demonstrate on an example how to use Golang template package to generate email content based on a template defined in a file. I'll keep example as simple as possible, but beefy enough to show most commonly used features of the package.

You can get full source code of the example from GitHub.

Go code

Below is Go code for the example:

package main

import (  
    "time"
    "html/template"
    "os"
    "fmt"
)

type Account struct {  
    FirstName string
    LastName  string
}

type Purchase struct {  
    Date          time.Time
    Description   string
    AmountInCents int
}

type Statement struct {  
    FromDate  time.Time
    ToDate    time.Time
    Account   Account
    Purchases []Purchase
}

func main() {  
    fmap := template.FuncMap{
        "formatAsDollars": formatAsDollars,
        "formatAsDate": formatAsDate,
        "urgentNote": urgentNote,
    }
    t := template.Must(template.New("email.tmpl").Funcs(fmap).ParseFiles("email.tmpl"))
    err := t.Execute(os.Stdout, createMockStatement())
    if err != nil {
        panic(err)
    }
}

func formatAsDollars(valueInCents int) (string, error) {  
    dollars := valueInCents / 100
    cents := valueInCents % 100
    return fmt.Sprintf("$%d.%2d", dollars, cents), nil
}

func formatAsDate(t time.Time) string {  
    year, month, day := t.Date()
    return fmt.Sprintf("%d/%d/%d", day, month, year)
}

func urgentNote(acc Account) string {  
    return fmt.Sprintf("You have earned 100 VIP points that can be used for purchases")
}

func createMockStatement() Statement {  
    return Statement{
        FromDate: time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC),
        ToDate: time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC),
        Account: Account{
            FirstName: "John",
            LastName: "Dow",
        },
        Purchases: []Purchase {
            Purchase{
                Date: time.Date(2016, 1, 3, 0, 0, 0, 0, time.UTC),
                Description: "Shovel",
                AmountInCents: 2326,
            },
            Purchase{
                Date: time.Date(2016, 1, 8, 0, 0, 0, 0, time.UTC),
                Description: "Staple remover",
                AmountInCents: 5432,
            },
        },
    }
}

The most important parts of Go code are data structures and main function. Other functions are less important and serve the only purpose to show different ways to call a function from a template.

Let's start with data structures representing a statement. Note that Statement includes an Account and a slice of Purchase.

Next let's zoom in on main function, where template creation and execution occurs. This method is a bit complicated, so we'll study it statement by statement.

    fmap := template.FuncMap{
        "formatAsDollars": formatAsDollars,
        "formatAsDate": formatAsDate,
        "urgentNote": urgentNote,
    }

Here we create a FuncMap, which is basically a map of function names to functions. When passed to a template, FuncMap allows it to call functions defined in the map.

Next statement is really complex, so I've split it into 2 pieces.

template.New("email.tmpl").Funcs(fmap).ParseFiles("email.tmpl")  

This snippet creates a new template with name email.tmpl and feeds it with the FuncMap created earlier. Then ParseFiles reads template from file email.tmpl and parses the template.

Note that template name and file name are the same. It's a trick to to tell ParseFiles not to create a new template, but reuse the one created with New function instead.

t := template.Must()  

ParseFiles returns (*Template, error), and Must function is a helper method which ensures that template is correct. Basically, Must panics if returned error is not nil.

err := t.Execute(os.Stdout, createMockStatement())  

The final piece applies template on a mock statement with output set to Stdout.

Template file

Note that template file email.tmpl below requires Go 1.6 or later.

{{with .Account -}}
Dear {{.FirstName}} {{.LastName}},  
{{- end}}

Below are your account statement details for period from {{.FromDate | formatAsDate}} to {{.ToDate | formatAsDate}}.

{{if .Purchases -}}
    Your purchases:
    {{- range .Purchases }}
        {{ .Date | formatAsDate}} {{ printf "%-20s" .Description }} {{.AmountInCents | formatAsDollars -}}
    {{- end}}
{{- else}}
You didn't make any purchases during the period.  
{{- end}}

{{$note := urgentNote .Account -}}
{{if $note -}}
Note: {{$note}}  
{{- end}}

Best Wishes,  
Customer Service  

As you can see, Go has chosen double braces ("{{" and "}}") to delimit data evaluation and control structures (known as actions) in templates. Some double braces have - attached, which tells Go to remove all spaces on the corresponding side.

Let's consider templating features one by one.

Current context

While rendering a template, Go uses a concept of current context. Current context is denoted by . and it's initial value is set to second parameter of Execute method. So .Account in our example references Account field of mock template created with createMockStatement().

Pipeline

Pipeline is a unique Go templating feature, which allows to declare expressions that can be executed in a manner similar to shell pipeline. Formally, a pipeline is a chained sequence of commands separated by | symbol. A command can be a simple value or a function call. The result of each command is passed as the last argument to the following command. The output of the final command in the pipeline is the value of the whole pipeline.

A command is allowed to return one or two values, the second of which must be of error type. If command returns two values and the second value evaluates to non-nil, execution terminates and the error is returned to the caller of Execute.

In our example, .FromDate | formatAsDate pipeline passes .FromDate as argument to formatAsDate function and the whole pipeline evaluates to result of the function.

with action
{{with pipeline}} T {{end}}

Sometime it's convenient to change current context and with does exactly that. In example we change current context to Account field, so that .FirstName and .LastName could be accessed directly (instead of .Account.FirstName and .Account.LastName).

if action
{{if pipeline}} T1 {{else}} T2 {{end}}

if action behaves similar to if statement in Go. The false values are false, 0, any string of length zero.

range action
{{range pipeline}} T {{end}}

range action (similar to range Go statement) loops through elements of the result of pipeline, and makes an element the new current context inside the loop.

Using variables
{{$variable := pipeline}}

You can define a variable inside a template. The syntax is similar to Go assignment.

Example output

After we examined all bits and pieces of the example, let's run it. Below is the output to be generated.

Dear John Dow,

Below are your account statement details for period from 1/1/2016 to 1/2/2016.

Your purchases:  
        3/1/2016 Shovel               $23.26
        8/1/2016 Staple remover       $54.32

Note: You have earned 100 VIP points that can be used for purchases

Best Wishes,  
Customer Service  
comments powered by Disqus