Templates: Using Actions, Functions, and Custom Functions

Alissa LozhkinAlissa Lozhkin
4 min read

My previous blog post introduced the feature of actions in templates. Another very similar feature is a function, which can be used together with an action to perform additional operations on the data. For example, {{ range }} is an action as it contributes to the overall structure of the template, whereas {{ len }} is a function as it performs a calculation. In this blog post, I will first demo how to use actions and functions together. I will then go over how to use custom functions in templates. We start with the following code in main.go:

package main

import (
    "os"
    "text/template"
    "strings"
)

type Person struct {
    Name       string
    Age        int
    Employed   bool
    Occupation string
    Interests  []string
}

func main() {
    people := []Person{
        {
            Name:       "Bob",
            Age:        42,
            Employed:   true,
            Occupation: "Doctor",
            Interests:  []string{"Reading", "Swimming"},
        },
        {
            Name:       "Jessica",
            Age:        22,
            Employed:   true,
            Occupation: "Teacher",
            Interests:  []string{"Piano", "Baking"},
        },
        {
            Name:       "Ben",
            Age:        23,
            Employed:   true,
            Occupation: "Photographer",
            Interests:  []string{"Music", "Running"},
        },
    }
    tmplFile := "demo.tmpl"
    tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
    if err != nil {
        panic(err)
    }
    err = tmpl.Execute(os.Stdout, people)
    if err != nil {
        panic(err)
    }
}

Note that I added an attribute called Interests (slice of strings) to the Person struct. For an explanation of this code, please read my previous post.

In demo.tmpl, we can use the slice function and the range action to loop over a section of the slice people.

{{ range slice . 1 2 }}
-----------------
Name: {{.Name}}
Age: {{.Age}}
Occupation: {{ if .Employed }}{{ .Occupation }}{{ else }}Unemployed{{end}}
{{ end }}

{{ slice . 1 2 }} is equivalent to people[1:2], and so calling range on this slice iterates through the second element of people and prints out the following string:

-----------------
Name: Jessica
Age: 22
Occupation: Teacher

For the purpose of printing out one element, it may be easier to use the index function. For example, the line {{ index . 0 }} is equivalent to people[0] and would print out {Bob 42 true Doctor [Reading Swimming]}.

Creating a plaintext report solely through the use of actions and built-in functions in templates can be quite restrictive as arithmetic operations cannot be used in templates directly.

Introducing the option to feed in custom functions to your templates. These functions can be defined however you want, and they give you the possibility to perform operations that are otherwise not allowed in templates.

Let’s say you want to index the last element of the slice you pass in. If you know the length of the slice, then you can simply hardcode the index number. But to make this more reusable, we need to pass in a custom function that allows us to decrement the length by one so that we can index the last element of any slice.

We start by defining a new function called sub in main.go:

func sub(i int) int {
    return i - 1
}

In the main function, we create a map fMap that maps a function name (string) to the function definition. We then pass this into the template through the template.New().Funcs function:

fMap := template.FuncMap{
    "sub": sub,
}
tmpl, err := template.New(tmplFile).Funcs(fMap).ParseFiles(tmplFile)
if err != nil {
    panic(err)
}
err = tmpl.Execute(os.Stdout, people)
if err != nil {
    panic(err)
}

In the template file, we write {{ len . | sub | index . }} , indicating that the length of the slice is passed to the sub function, and the output of the sub function is passed to the index function. Therefore, when passing in the slice people, the output of this line is people[2] or { Ben 23 true Photographer [Music Running]}.

In a similar way, we can pass built-in Go functions into a template. For example, let’s say we want to convert the slice of Interests into a string where each interest is separated by & . To do this, we first pass in the function strings.Join to the template by adding a key-value pair to fMap like so:

fMap := template.FuncMap{
    "sub":  sub,
    "join": strings.Join,
}

Going back to demo.tmpl, this time looping through the whole slice, we can use this function to return the desired string:

{{ range . }}
-----------------
Name: {{.Name}}
Age: {{.Age}}
Occupation: {{ if .Employed }}{{ .Occupation }}{{ else }}Unemployed{{end}}
Interests: {{ join .Interests " & " }}
{{ end }}

Saving demo.tmpl and running go run main.go gives the following result:

-----------------
Name: Bob
Age: 42
Occupation: Doctor
Interests: Reading & Swimming

-----------------
Name: Jessica
Age: 22
Occupation: Teacher
Interests: Piano & Baking

-----------------
Name: Ben
Age: 23
Occupation: Photographer
Interests: Music & Running
0
Subscribe to my newsletter

Read articles from Alissa Lozhkin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alissa Lozhkin
Alissa Lozhkin

My name is Alissa and I am a fourth-year computer science and math student at the University of Toronto! I am very interested in computer science and software development, and I love learning about new technologies! Get in touch with me on LinkedIn: https://www.linkedin.com/in/alissa-lozhkin