Gotya: Go Templates

2020-08-02 — 5 mins reading time

Since I started to use Hugo for creating this web site, I more or less willingly came into contact with Go Templates. I then thought, that this could be a good solution for generating LaTeX invoices: simply use placeholders in a tex file and replace them with actual values using Go Templates. Thus, as my first Go project, I decided to build a simple tool first: It shall take variables from a YAML input file and then generate a text file based on a template referencing these variables.

The code/files I mention in this article are also available on Github. If you don’t want to copy/paste everything, you can simply checkout the repository.

The target picture

Let’s define a simple set of variables in a YAML file (values.yaml):

1
2
3
4
5
6
7
myStr: abc
myInt: 123
myObj:
  valA: Value A
  myArray:
  - 42
  - 23

Thus, I want to be able to use strings as well as numbers and I shall also be able to create complex objects and arrays (which becomes important when you have a long list of variables for each item on your invoice).

As test template, let’s use the following one (template.txt):

1
2
3
4
5
6
7
8
My test string: {{ .myStr }}

My test integer incremented by 1: {{ add .myInt 1 }}

My test array:
{{ range .myObj.myArray -}}
* {{ . }}
{{ end }}

This is the standard feature set of Go Templates, with the addition of the function add which adds two numbers. Interestingly, Go Templates does not offer arithmetics in the template itself. Anyway, it will show that the template language can be easily extended.

The actual Go program

This won’t be a Go tutorial, so you might want to read up a few things first. And since the code is more or less straight forward, I simply paste it here in it’s entirety (save it as main.go):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"text/template"

	"gopkg.in/yaml.v2"
)

type Values map[string]interface{}

func main() {
	if len(os.Args) != 3 {
		fmt.Println("Usage: " + os.Args[0] + " [values file (yaml)] [template file]")
		os.Exit(1)
	}
	valuesFile := os.Args[1]
	templateFile := os.Args[2]

	valuesData, err := ioutil.ReadFile(valuesFile)
	if err != nil {
		fmt.Printf("Failed to read %v: %v\n", valuesFile, err)
		os.Exit(1)
	}

	values := Values{}
	err = yaml.Unmarshal([]byte(valuesData), &values)
	if err != nil {
		fmt.Printf("Failed to parse %v: %v\n", valuesFile, err)
		os.Exit(1)
	}

	tmplData, err := ioutil.ReadFile(templateFile)
	if err != nil {
		fmt.Printf("Failed to read %v: %v\n", templateFile, err)
		os.Exit(1)
	}

	// custom function that can be called from within the template
	funcMap := template.FuncMap{
		"add": func(a int, b int) int {
			return a + b
		},
	}

	tmpl := template.New(templateFile).Funcs(funcMap)
	tmpl, err = tmpl.Parse(string(tmplData))
	if err != nil {
		fmt.Printf("Failed to parse template file: %v\n", err)
		os.Exit(1)
	}

	out := new(bytes.Buffer)
	err = tmpl.Execute(out, values)
	if err != nil {
		fmt.Printf("Failed to apply values to template: %v\n", err)
		os.Exit(1)
	}

	fmt.Print(out)
}

Before you can compile it, you still need to install the YAML package:

1
$ go get gopkg.in/yaml.v2

After that, you can build and run it:

1
2
$ go build -o gotya main.go
$ ./gotya values.yaml template.txt

Why gotya? Well, “Go Template tool, Yet Another one” ;-)

The output:

1
2
3
4
5
6
7
My test string: abc

My test integer incremented by 1: 124

My test array:
* 42
* 23

Some interesting things

13
	type Values map[string]interface{}

Go is a typed language, and normally, we would define a struct with all the variables and provide this to the template.Execute function. However, we do not know the YAML file’s content beforehand, so we declare an interface as a map type.

42
43
44
45
46
47
	// custom function that can be called from within the template
	funcMap := template.FuncMap{
		"add": func(a int, b int) int {
			return a + b
		},
	}

We can extend the template language by custom functions. This is certainly just a simple example, but basically you can define whatever complex function you need.

And now with LaTeX

The beauty of the above program: You can use it with whatever text file you like. Almost.

LaTeX actually makes heavy use curly braces, and as you can see above, the Go template languages does the same in its standard configuration. Luckily, the delimiters of the template language can be changed. Simply provide them during instantiation of the template object:

42
tmpl := template.New(templateFile).Funcs(funcMap).Delims("[[", "]]")

The actual invoice in LaTeX

This will be part of a later article, so stay tuned.

Alternatives

The initial problem could be actually solved in many different ways. There are many other text templating tools around and in production use.

One of them for the Go template language is gomplate, also written in Go. This is the above example on steroids. It already has loads of integrated custom functions as well as support for many more data sources, including environment variables (important in the world of Docker), JSON and YAML. Thus, you might want to consider using this one instead of writing one of your own.

Another one is j2cli which is based on Jinja and supports also the use of environment variables. The syntax of Jinja is different, so it is more a matter of what kind of syntax you are already used to. Plus, you need python as a dependency to use j2cli.

Go programs, by nature, are statically linked and thus standalone. Which not always is an advantage since it results in larger-than-usual executable files. gomplate, for instance has a file size of about 35 MB (v3.7.0), our above example code already results in about 4 MB – we are talking about command line tools here without any fancy UI.