DEV Community

Kirk Lewis
Kirk Lewis

Posted on

Go Text Template Processing

I recently worked on a project where I used Go's template package for parsing and interpolating templates. After processing the templates, I needed to use the interpolated string result for saving new file content, and also naming directories and files. This post is a walkthrough of the functions I wrote to help process text templates, then afterwards each function is demonstrated.

Go Project Setup

Since I am demonstrating these functions outside the project I created them for, I've created a simpler project for this post as follows:

go-template-processing/
├── main.go
├── src
│   └── demo
│       └── template
│           └── process.go
└── templates
    └── got.tmpl
Enter fullscreen mode Exit fullscreen mode

Create the Template File

Add the following text to the file templates/got.tmpl. This file template will be processed later.

{{.Name}} of house {{.House}}

{{if .Traits -}}
Traits:
    {{- range .Traits}}
    * {{. -}}
    {{end}}
{{end}}
Enter fullscreen mode Exit fullscreen mode

Anatomy of the Template

The annotations like {{.Name}} and {{.House}} will be replaced with their respective values, during template execution. The other annotations like {{if}} and {{range}} know as Actions, will also be evaluated. The hyphens - to either side of the {{ and }} delimiters, are used to trim any space before and after them respectively. The dot . represents the current element in the range of Traits being iterated.

Set the GOPATH

cd go-template-processing
export GOPATH=$PWD
Enter fullscreen mode Exit fullscreen mode

Interpolating the Parsed Template

To separate the concern of first parsing then interpolating a template file or string, I've created a function in the file process.go which does the interpolation first.

Go refers to the this interpolation as "executing the template", where the template is applied to a data structure.

package template

import (
    "bytes"
    "text/template"
)

// process applies the data structure 'vars' onto an already
// parsed template 't', and returns the resulting string.
func process(t *template.Template, vars interface{}) string {
    var tmplBytes bytes.Buffer

    err := t.Execute(&tmplBytes, vars)
    if err != nil {
        panic(err)
    }
    return tmplBytes.String()
}
Enter fullscreen mode Exit fullscreen mode

The process function should be easy to reason with given its comment, annotations and implementation.

The argument vars is typed to an empty interface, so that any type like map or Struct with fields corresponding to the template's annotations can be used. Any {{.Annotation}} without a corresponding field is replaced with <no value>, or panics if it fails evaluation.

Writing the process function above is a pre-emptive extract method refactoring done to avoid DRY occurring later in the code.

Processing a Template String

The next function added to the process.go file, will process a template string by creating a new Template and then parsing the given string str argument. The new Template assigned to tmpl is then interpolated using the private process function created earlier.

func ProcessString(str string, vars interface{}) string {
    tmpl, err := template.New("tmpl").Parse(str)

    if err != nil {
        panic(err)
    }
    return process(tmpl, vars)
}
Enter fullscreen mode Exit fullscreen mode

This function will be demonstrated later.

Processing a Template File

This function is very similar to ProcessString but it processes the file indicated by the fileName argument.

The ParseFiles function can accept more than one file name argument. However, I only need to parse one file at a time, so I am using a single fileName argument.

func ProcessFile(fileName string, vars interface{}) string {
    tmpl, err := template.ParseFiles(fileName)

    if err != nil {
        panic(err)
    }
    return process(tmpl, vars)
}
Enter fullscreen mode Exit fullscreen mode

That's it, both functions can now be consumed by client code.

Using the Process Functions

The ./main.go file below demonstrates how both process functions are used.

package main

import (
    "fmt"
    "demo/template"
)

func main() {
    // data structure the template will be applied to
    vars := make(map[string]interface{})
    vars["Name"] = "Brienne"
    vars["House"] = "Tarth"
    vars["Traits"] = []string{"Brave", "Loyal"}

    // process a template string
    resultA := template.ProcessString("{{.Name}} of house {{.House}}", vars)

    // process a template file
    resultB := template.ProcessFile("templates/got.tmpl", vars)

    fmt.Println(resultA, "\n")
    fmt.Println(resultB)
}
Enter fullscreen mode Exit fullscreen mode

Running go run main.go should output the following:

Brienne of house Tarth

Brienne of house Tarth

Traits:
    * Brave
    * Loyal
Enter fullscreen mode Exit fullscreen mode

Both the template string and file have been successfully processed. The resulting string can be now used for an email body, the contents of a file, etc.

Supplying Vars via a Struct

Once again, since vars is typed to an empty interface, a struct can be used here also.

I've added the following snippet just below the fmt.Println(resultB) statement in main.go.

// using a Struct
type Westerosi struct {
    Name   string
    House  string
    Traits []string
}

jorah := Westerosi{"Ser Jorah", "Mormont", []string{"Brave", "Protective"}}
resultC := template.ProcessFile("templates/got.tmpl", jorah)

fmt.Println(resultC)
Enter fullscreen mode Exit fullscreen mode

Running go run main.go again should now output the following:

Brienne of house Tarth

Brienne of house Tarth

Traits:
    * Brave
    * Loyal

Ser Jorah of house Mormont

Traits:
    * Brave
    * Protective
Enter fullscreen mode Exit fullscreen mode

Conclusion

Working with Go templates is pretty straight forward out of the box, making it easy to build your own project specific functions. While I have only covered a specific task in this post, you can read more about the Go template package at https://golang.org/pkg/text/template.

The code used in the article is available on Github.
Go version used at time of writing go version go1.12.1

Thank you for reading.

Top comments (0)