Go templates are a powerful tool to customize output the way you want it. It’s a builtin package implements data-driven templates. Templates are executed by applying them to a data structure.
While there are articles covering the basics, I had a hard time findings material on more advanced use-cases, such as looping over complex structs or using a function in the template. This post aims to distill these advanced use-cases with examples.
If you’re unfamiliar with templates, this blog post covers a great introduction to it.
This is our first use-case: I need to provide configuration to Thanos about the sidecars. It’s basically a list of servers Thanos needs to communicate with. I have hundreds of servers. If you’re not familiar with Thanos, that’s okay. It’s not important for this post. I use it just to show a real-world example.
The template and data
We start by creating a template, then the data it’s gonna execute with. Our output would be a YAML configuration file.
Here is an example of how to iterate a slice and use it in a template:
|
|
Using the range
action we iterate an object. In my case, it’s a slice of strings. Inside the {{ range .. }}
block we refer to items
as .
Passing data to the template
In the previous example, we could simply pass in the slice to the template. But I used a struct. This allows me to extend my template with minimal changes to the code.
At the time of writing, the template engine supports up to 1 argument. If you need to provide multiple values, use a struct.
Another (more complex) alternative is this answer on Stackoverflow.
Let’s say instead of templating the location names, we want their respective IPs. This requires a few changes in our code. We would need to update our data struct to a map, of locations to IPs, and the template code too:
|
|
Here I loop over a map and extract the relevant values. Cool.
Important thing to note is, we define $location
and $ip
variables in the {{ range .. }}
statement.
They will be available in the template only in the context of the range block. You can’t use them outside this scope:
A variable’s scope extends to the “end” action of the control structure (“if”, “with”, or “range”) in which it is declared, or to the end of the template if there is no such control structure.
A template invocation does not inherit variables from the point of its invocation.
When execution begins, $ is set to the data argument passed to Execute, that is, to the starting value of dot.
Access multiple fields of a data structure
Our requirements have changed. Now we don’t want to template all the locations of the IPAddrs
map, but only the selected ones (enabled).
We need to modify our data. Our data struct contains all the locations (slice) and their relevant IP addresses (map).
A better name for our Locations
field would be EnabledLocations
. We change that accordingly.
Now we want to template an IP address only if the location it resides is enabled. We would need to:
- Iterate a slice (EnabledLocations)
- Fetch relevant data from the map (IPAddrs)
- Template the data in our template
|
|
I have done a few things here:
- Remove
London
from theEnabledLocations
slice - Iterate
.EnabledLocations
slice withrange
action and save current element to a variable$location
- Define a new variable
$ip
with the value from a map with a key$location
- Inside the
{{ range .. }}
clause the scope is changed..
cursor now reference the current item in the loop (in this example, it is equal to$location
) - To access the outer scope, I use
$.
— that way I can access theIPAddrs
map - index function returns the result of indexing its first argument by the following arguments. In other words, the first argument is the key, and the latter is a map, slice, or array
- Inside the
Let’s complicate things a little more. Let’s say our setup grew with more servers per location. We need our template to support multiple IPs per location. This requires changes to our data structure and template once again.
How can we use a dynamic key to fetch values from our map?
|
|
Here is what changed:
IPAddrs
is a map of string to slice of strings- Define the
$ipList
variable which contains the relevant string slice (using theindex
function) - I got 2 range loops now: one loop the locations, the other loop each location’s IP list
You can also call methods inside a template, here’s a clear example from StackOverflow how to do it.
Summary
There are multiple ways to template data. Go provides a rich builtin library worth exploring.
Make sure the read the docs before banging your head over syntax, or other builtin capabilities such as index
, range
, and many more I haven’t covered in this post.
Actually, the library is much richer than what I present here. Here are the takeaways:
range
action is used to iterate data structuresindex
function is used to extract data from a map, slice, or array$var :=
is used to define a variable and use it inside a template- functions can be executed inside templates