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
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}}
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
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()
}
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)
}
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)
}
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)
}
Running go run main.go
should output the following:
Brienne of house Tarth
Brienne of house Tarth
Traits:
* Brave
* Loyal
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)
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
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)