Oluwatomisin Bamimore I'm a full-stack Python developer, technical writer, and a Section Engineering Education contributor.

How to use Go text templates

7 min read 2011

Using Golang Templates

Templates are files that define a specific pattern and give room for automation, be it a simple text file or an HTML file for a webpage. If it provides space for customization, it is a template.

You can write programming logic to parse simple templates, but as the level of customization in your template increases, the programming logic you need to customize increases, making it less feasible to write template parsing logic.

Programming languages and web development frameworks support template parsing out of the box or provide libraries for template parsing.

In this article, we will explore features in the Go programming language that support template parsing. We can use these methods to parse any file extension, but we will use only text files in this article.

This guide will also only display Go code and the least amount of template (text) needed to understand the Go code.

Go template tutorial prerequisites

Before beginning our tutorial, you should have:

You can also clone the guide’s repository to access the complete template files or enter:

git clone https://github.com/Bamimore-Tomi/go-templates-guide.git

Working with Go text templates

In this section, we’ll explore the features of the text/template package in Go.

Using ParseFiles in Go

To work with templates, you must parse them into your Go program.

The text/template standard library provides the functions needed to parse our program:



package main
import (
    "log"
    "os"
    "text/template"
)
// Prints out the template without passing any value using the text/template package
func main() {
    template, err := template.ParseFiles("template-01.txt")
    // Capture any error
    if err != nil {
        log.Fatalln(err)
    }
    // Print out the template to std
    template.Execute(os.Stdout, nil)
}
//OUTPUT
// Hi <no value>
// You are welcome to this tutorial

The program above prints a template file called template-01.txt. The template variable holds the content of the file. To print out the file to Stdout, we call the Execute method.

Using ParseGlob

For parsing multiple files at once, the ParseGlob function is useful:

package main
import (
    "log"
    "os"
    "text/template"
)
// Parse all the files in a certain directory
func main() {
    // This function takes a pattern. It can be a folder
    temp, err := template.ParseGlob("templates/*")
    if err != nil {
        log.Fatalln(err)
    }
    // Simply calling execute parses the first file in the directory
    err = temp.Execute(os.Stdout, nil)
    if err != nil {
        log.Fatalln(err)
    }
    // Or you can execute a particular template in the directory
    err = temp.ExecuteTemplate(os.Stdout, "template-03.txt", nil)
    if err != nil {
        log.Fatalln(err)
    }
    // Calling a template not in the directory will produce an error
    err = temp.ExecuteTemplate(os.Stdout, "template-04.txt", nil)
    if err != nil {
        log.Fatalln(err)
    }
}

With this code, we parsed all the files in the templates/ directory to our program. To execute any of the templates parsed, we call the ExecuteTemplate method on the result of ParseGlob.

Using the Execute method in Go

The Execute method is where we parse data into the template(s). Here is the template-04.txt file:

Hello {{.}}

You are doing great. Keep learning.
Do not stop {{.}}

The {{.}} tells the text/template package where to place the data that is passed into the template. In this template, we want to set the data in two places: line 1 and line 4:

package main
import (
    "log"
    "os"
    "text/template"
)
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the reponse of template.ParseFiles and does error checking
    temp = template.Must(template.ParseFiles("template-04.txt"))
}
func main() {
    // Execute myName into the template and print to Stdout
    myName := "Oluwatomisin"
    err := temp.Execute(os.Stdout, myName)
    if err != nil {
        log.Fatalln(err)
    }
}
// Hello Oluwatomisin
// You are doing great. Keep learning.
// Do not stop Oluwatomisin

Here, we use slightly different syntax to initialize the template. temp.Execute takes an io.writer and data interface{}, which is myName in this case.

You can also pass in more complicated data structures in your template; the only change to do this is how you access the structures inside the template file, which we will discuss in the next section.

Declaring variables inside Go templates

We can also initialize variables right inside a template. Check the example in template-05.txt:


More great articles from LogRocket:


Hello {{.Name}}

{{$aTrait := .Trait}}
You are {{$aTrait}}

Notice a change in how we use data inside the template. template.Execute takes a data interface{} argument, which means we can execute a struct in a template:

package main
import (
    "log"
    "os"
    "text/template"
)
type PersonTrait struct {
    Name string
    Trait string
}
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the reponse of template.ParseFiles and does error checking
    temp = template.Must(template.ParseFiles("template-05.txt"))
}
func main() {
    // Execute person into the template and print to Stdout
    person := PersonTrait{Name: "Oluwatomisin", Trait: "a great writer"}
    err := temp.Execute(os.Stdout, person)
    if err != nil {
        log.Fatalln(err)
    }
}
// Hello Oluwatomisin
// You are a great writer

In this example, we execute the PersonTrait struct in the template. This way, you can execute any data type inside a template.

Using loops in Go templates

The text/template package also allows you to run loops inside a template. In template-06.txt, we will list some cute pets:

Animals are cute; some cute animals are:
{{range .}}
{{.}}
{{end}}

In the program, we must execute a slice of cute pets in the template:

package main
import (
    "log"
    "os"
    "text/template"
)
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the reponse of template.ParseFiles and does error checking
    temp = template.Must(template.ParseFiles("template-06.txt"))
}
func main() {
    // Execute cuteAnimals into the template and print to Stdout
    cuteAnimals := []string{"Dogs", "Cats", "Mice", "Fish"}
    err := temp.Execute(os.Stdout, cuteAnimals)
    if err != nil {
        log.Fatalln(err)
    }
}
// Animals are cute, some cute animals are:
// Dogs
// Cats
// Mice
// Fish

We can also loop through a map if needed:

Animals are cute, some cute animals are:
{{range $key, $val := .}}
{{$key}} , {{$val}}
{{end}}

Then, we can create and execute the map in the program:

package main
import (
    "log"
    "os"
    "text/template"
)
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the response of template.ParseFiles and does error checking
    temp = template.Must(template.ParseFiles("template-06.txt"))
}
func main() {
    // Execute cuteAnimalsSpecies into the template and print to Stdout
    cuteAnimalsSpecies := map[string]string{
        "Dogs": "German Shepherd",
        "Cats": "Ragdoll",
        "Mice": "Deer Mouse",
        "Fish": "Goldfish",
    }
    err := temp.Execute(os.Stdout, cuteAnimalsSpecies)
    if err != nil {
        log.Fatalln(err)
    }
}
// Animals are cute, some cute animals are:
// Cats , Ragdoll
// Dogs , German Shepherd
// Fish , Goldfish
// Mice , Deer Mouse

Conditional statements in Go templates

To add more customization to our templates, we can use conditional statements. To use conditionals, we must call the comparison functions inside the template. In this example, we can check whether a random integer is less or greater than 200:

{{if (lt . 200)}}
Number {{.}} is less than 200
{{else}}
Number {{.}} is greater than 200
{{end}}

The (lt . 200) syntax is how we compare the random integer value using lt. Other operators include:

  • lt for the less-than operator
  • gt for the greater-than operator
  • eq for the equals-o operator
  • ne for the not equals-to operator
  • le for the less-than-or-equal-to operator
  • ge for the greater-than-or-equal-to operator

Now, we can generate the random value in our main program:

package main
import (
    "log"
    "math/rand"
    "os"
    "text/template"
    "time"
)
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the reponse of template.ParseFiles and does error checking
    temp = template.Must(template.ParseFiles("template-06.txt"))
}
func main() {
    // Generate random number between 100 and 300
    rand.Seed(time.Now().UnixNano())
    min := 100
    max := 300
    // Execute myNumber into the template and print to Stdout
    myNumber := rand.Intn((max-min)+1) + min
    err := temp.Execute(os.Stdout, myNumber)
    if err != nil {
        log.Fatalln(err)
    }
}
// Number 141 is less than 200

Using Go functions in templates

The text/template package also provides a way to execute custom functions inside a template. A famous example is converting timestamps into other date formats:

Hi,

Time before formatting : {{.}}
Time After formatting : {{formatDate .}}

The template displays the time before and after parsing with the formatDate function. Observe the syntax that can call the custom function:

package main
import (
    "log"
    "os"
    "text/template"
    "time"
)
// Declare type pointer to a template
var temp *template.Template
// Using the init function to make sure the template is only parsed once in the program
func init() {
    // template.Must takes the reponse of template.ParseFiles and does error checking
    temp = template.Must(template.New("template-07.txt").Funcs(funcMap).ParseFiles("template-08.txt"))
}
// Custom function must have only 1 return value, or 1 return value and an error
func formatDate(timeStamp time.Time) string {
    //Define layout for formatting timestamp to string
    return timeStamp.Format("01-02-2006")
}
// Map name formatDate to formatDate function above
var funcMap = template.FuncMap{
    "formatDate": formatDate,
}
func main() {
    timeNow := time.Now()
    err := temp.Execute(os.Stdout, timeNow)
    if err != nil {
        log.Fatalln(err)
    }
}
// Hi,
// Time before formatting : 2021-10-04 18:01:59.6659258 +0100 WAT m=+0.004952101
// Time After formatting: 09-04-2021

The layout format must follow the timestamp format — in this case, Jan 2 15:04:05 2006 MST. Check the method’s documentation for more information.

Here, however, we use template.FuncMap to map a string to the custom function. The function will then be referenced using the string inside the template.

Customizing emails with Go

Whenever you see your favorite newsletter that is customized to you, chances are an email template and a templating processing package like text/template in Go made it look that way. The following program will customize a newsletter from the various techniques and methods in the text/template package.

package main
 
import (
   "fmt"
   "os"
   "text/template"
   "time"
)
 
// Declare type pointer to a template
var temp *template.Template
 
// Using the init function to make sure the template is only parsed once in the program
func init() {
   // template.Must takes the reponse of template.ParseFiles and does error checking
   temp = template.Must(template.New("template-08.txt").Funcs(funcMap).ParseFiles("template-08.txt"))
}
 
// Custom function must have only 1 return value, or 1 return value and an error
func formatDate(timeStamp time.Time) string {
   //Define layout for formatting timestamp to string
   // return timeStamp.Format("01-02-2006")
   return timeStamp.Format("Mon, 02 Jan 2006")
 
}
 
// Map name formatDate to formatDate function above
var funcMap = template.FuncMap{
   "formatDate": formatDate,
}
 
type customer struct {
   Username             string
   Subscriptions        []string
   LastLoginDays        int
   SubscriptionDeadline time.Time
}
 
func check(e error) {
   if e != nil {
       panic(e)
   }
}
 
func main() {
   customers := []customer{
       {
           Username:             "wisman",
           Subscriptions:        []string{"Kikat", "Lollipop", "Marshmallow", "Oreo", "Jelly Bean"},
           LastLoginDays:        10,
           SubscriptionDeadline: time.Now(),
       },
       {
           Username:             "berry blings",
           Subscriptions:        []string{"Honeycomb", "Gingerbread", "Nougat", "Froyo", "Donut"},
           LastLoginDays:        5,
           SubscriptionDeadline: time.Date(2023, 3, 15, 5, 5, 5, 3, time.UTC),
       },
       {
           Username:             "savy",
           Subscriptions:        []string{"Honeycomb", "Gingerbread", "Nougat", "Froyo", "Donut"},
           LastLoginDays:        5,
           SubscriptionDeadline: time.Date(2023, 6, 15, 5, 5, 5, 3, time.UTC),
       },
   }
   for _, user := range customers {
       // Create a new file for each customer
       file, err := os.Create(fmt.Sprintf("output/%v.txt", user.Username))
       check(err)
       // Execute the template for each user into their respective files
       err = temp.Execute(file, user)
       check(err)
   }
}

The program executes an email template using the customers’ data.

Hi {{.Username}},

We notice you last logged in {{.LastLoginDays}} days ago.

Your current subscriptions:
{{range $subscription := .Subscriptions}}{{$subscription}}
{{end}}
expires on {{formatDate .SubscriptionDeadline}}.

After running the program, it outputs a file for each customer in the array of customers:

customizing email templates in Go

Conclusion

We have seen a range of features in the text/template package with usage examples. All the features implemented here are the same for the html/template package. Outputs generated from html/template are safe from cross-site scripting by providing auto-escaping and context-sensitive sanitization of inputs.

When using templates in the web context where the output is HTML, use the html/template package.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Oluwatomisin Bamimore I'm a full-stack Python developer, technical writer, and a Section Engineering Education contributor.

Leave a Reply