Overview
Text is all around us as software developers. Code is text, HTML is text, XNL/JSON/YAML/TOML is text, Markdown is text, CSV is text. All these text formats are designed to cater to both humans and machines. Humans should be able to read and edit textual formats with plain text editors.
But there are many cases where you need to generate text in a certain format. You may convert from one format to another, create your own DSL, generate some helper code automatically, or just customize an email with user-specific information. Whatever the need is, Go is more than able to assist you along the way with its powerful templates.
In this tutorial, you’ll learn about the ins and outs of Go templates and how to use them for powerful text generation.
What Are Go Templates?
Go templates are objects that manage some text with special placeholders called actions, which are enclosed by double curly braces: {{ some action }}
. When you execute the template, you provide it with a Go struct that has the data the placeholders need.
Here’s a quick example that generates knock knock jokes. A knock knock joke has a very strict format. The only things that change are the identity of the knocker and the punchline.
package main import ( "text/template" "os" ) type Joke struct { Who string Punchline string } func main() { t := template.New("Knock Knock Joke") text := `Knock KnocknWho's there? {{.Who}} {{.Who}} who? {{.Punchline}} ` t.Parse(text) jokes := []Joke{ {"Etch", "Bless you!"}, {"Cow goes", "No, cow goes moo!"}, } for _, joke := range jokes { t.Execute(os.Stdout, joke) } } Output: Knock Knock Who's there? Etch Etch who? Bless you! Knock Knock Who's there? Cow goes Cow goes who? No, cow goes moo!
Understanding Template Actions
The template syntax is very powerful, and it supports actions such as data accessors, functions, pipelines, variables, conditionals, and loops.
Data Accessors
Data accessors are very simple. They just pull data out of the struct starting. They can drill into nested structs too:
func main() { family := Family{ Father: Person{"Tarzan"}, Mother: Person{"Jane"}, ChildrenCount: 2, } t := template.New("Father") text := "The father's name is {{.Father.Name}}" t.Parse(text) t.Execute(os.Stdout, family) }
If the data is not a struct, you can use just {{.}}
to access the value directly:
func main() { t := template.New("") t.Parse("Anything goes: {{.}}n") t.Execute(os.Stdout, 1) t.Execute(os.Stdout, "two") t.Execute(os.Stdout, 3.0) t.Execute(os.Stdout, map[string]int{"four": 4}) } Output: Anything goes: 1 Anything goes: two Anything goes: 3 Anything goes: map[four:4]
We will see later how to deal with arrays, slices, and maps.
Functions
Functions really elevate what you can do with templates. There are many global functions, and you can even add template-specific functions. The complete list of global functions is available on the Go website.
Here is an example of how to use the printf
function in a template:
func main() { t := template.New("") t.Parse(`Keeping just 2 decimals of π: {{printf "%.2f" .}} `) t.Execute(os.Stdout, math.Pi) } Output: Keeping just 2 decimals of π: 3.14
Pipelines
Pipelines let you apply multiple functions to the current value. Combining different functions significantly expands the ways in which you can slice and dice your values.
In the following code, I chain three functions. First, the call function executes the function pass to Execute()
. Then the len
function returns the length of the result of the input function, which is 3 in this case. Finally, the printf
function prints the number of items.
func main() { t := template.New("") t.Parse(`{{ call . | len | printf "%d items" }} `) t.Execute(os.Stdout, func() string { return "abc" }) } Output: 3 items
Variables
Sometimes you want to reuse the result of a complex pipeline multiple times. With Go templates, you can define a variable and reuse it as many times as you want. The following example extracts the first and last name from the input struct, quotes them, and stores them in the variables $F
and $L
. Then it renders them in normal and reverse order.
Another neat trick here is that I pass an anonymous struct to the template to make the code more concise and avoid cluttering it with types that are used only in one place.
func main() { t := template.New("") t.Parse(`{{ $F := .FirstName | printf "%q"}} {{ $L := .LastName | printf "%q"}} Normal: {{$F}} {{$L}} Reverse: {{$L}} {{$F}}` ) t.Execute(os.Stdout, struct { FirstName string LastName string }{ "Gigi", "Sayfan", }) } Output: Normal: "Gigi" "Sayfan" Reverse: "Sayfan" "Gigi"
Conditionals
But let’s not stop here. You can even have conditions in your templates. There is an if-end
action and if-else-end
action. The if clause is displayed if the output of the conditional pipeline is not empty:
func main() { t := template.New("") t.Parse(`{{ if . -}} {{ . }} {{ else }} No data is available {{ end }}` ) t.Execute(os.Stdout, "42") t.Execute(os.Stdout, "") } Output: 42 No data is available
Note that the else clause causes a new line, and the “No data available” text is significantly indented.
Loops
Go templates have loops too. This is super useful when your data contains slices, maps, or other iterables. The data object for a loop can be any iterable Go object like array, slice, map, or channel. The range function allows you to iterate over the data object and create an output for each element. Let’s see how to iterate over a map:
func main() { t := template.New("") e := `Name,Scores {{range $k, $v := .}}{{$k}} {{range $s := $v}},{{$s}}{{end}} {{end}} ` t.Parse(e) t.Execute(os.Stdout, map[string][]int{ "Mike": {88, 77, 99}, "Betty": {54, 96, 78}, "Jake": {89, 67, 93}, }) } Output: Name,Scores Betty,54,96,78 Jake,89,67,93 Mike,88,77,99
As you can see, the leading whitespace is still a problem. I wasn’t able to find a decent way to address it within the template syntax. It will require post-processing. In theory, you can place a dash to trim whitespace preceding or following actions, but it doesn’t work in the presence of range
.
Text Templates
Text templates are implemented in the text/template package. In addition to everything we’ve seen so far, this package can also load templates from files and compose multiple templates using the template action. The Template object itself has many methods to support such advanced use cases:
- ParseFiles()
- ParseGlob()
- AddParseTree()
- Clone()
- DefinedTemplates()
- Delims()
- ExecuteTemplate()
- Funcs()
- Lookup()
- Option()
- Templates()
Due to space limitations, I will not go into further detail (maybe in another tutorial).
HTML Templates
HTML templates are defined in the html/template package. It has exactly the same interface as the text template package, but it is designed to generate HTML that is safe from code injection. This is done by carefully sanitizing the data before embedding it in the template. The working assumption is that template authors are trusted, but the data provided to the template can’t be trusted.
This is important. If you automatically apply templates you receive from untrusted sources then the html/template package will not protect you. It is your responsibility to check the templates.
Let’s see the difference between the output of text/template
and html/template
. When using the text/template, it’s easy to inject JavaScript code into the generated output.
package main import ( "text/template" "os" ) func main() { t, _ := template.New("").Parse("Hello, {{.}}!") d := "" t.Execute(os.Stdout, d) } Output: Hello, !
But importing the html/template
instead of text/template
prevents this attack, by escaping the script tags and the parentheses:
Hello, <script>alert('pwened!')</script>!
Dealing With Errors
There are two types of errors: parsing errors and execution errors. The Parse()
function parses the template text and returns an error, which I ignored in the code samples, but in production code you want to catch these errors early and address them.
If you want a quick and dirty exit then the Must()
method takes the output of a method that returns (*Template, error)
—like Clone()
, Parse()
, or ParseFiles()
—and panics if the error is not nil. Here is how you check for an explicit parsing error:
func main() { e := "I'm a bad template, }}{{" _, err := template.New("").Parse(e) if err != nil { msg := "Failed to parsing: '%s'.nError: %vn" fmt.Printf(msg, e, err) } } Output: Failed to parse template: 'I'm a bad template, }}{{'. Error: template: :1: unexpected unclosed action in command
Using Must()
just panics if something is wrong with the template:
func main() { e := "I'm a bad template, }}{{" template.Must(template.New("").Parse(e)) } Output: panic: template: :1: unexpected unclosed action in command
The other kind of error is an execution error if the provided data doesn’t match the template. Again, you can check explicitly or use Must()
to panic. I recommend in this case that you check and have a recovery mechanism in place.
Usually, there is no need to bring the whole system down just because an input doesn’t meet the requirements. In the following example, the template expects a field called Name
on the data struct, but I provide a struct with a field called FullName
.
func main() { e := "There must be a name: {{.Name}}" t, _ := template.New("").Parse(e) err := t.Execute( os.Stdout, struct{ FullName string }{"Gigi Sayfan"}, ) if err != nil { fmt.Println("Fail to execute.", err) } } Output: There must be a name: Fail to execute. template: :1:24: executing "" at <.Name>: can't evaluate field Name in type struct { FullName string }
Conclusion
Go has a powerful and sophisticated templating system. It is used to great effect in many large projects like Kubernetes and Hugo. The html/template package provides a secure, industrial-strength facility to sanitize the output of web-based systems. In this tutorial, we covered all the basics and some intermediate use cases.
There are still more advanced features in the template packages waiting to be unlocked. Play with templates and incorporate them into your programs. You will be pleasantly surprised how concise and readable your text-generation code looks.
Powered by WPeMatico