With the prevalence of Restful architecture in recent years, front-end and back-end separation has become popular, and template rendering has been shifted from the back-end to the front-end, where the back-end only needs to provide resource data, resulting in traditional server-side template scripting languages like JSP and PHP being almost unused. However, in Go, template rendering is not limited to server-side markup languages (e.g. HTML), but GO often uses templating languages to handle, for example, text transformations that insert specific data. While not as flexible as regular expressions, rendering is far more efficient and simpler to use than regular expressions. It is very friendly for certain cloud computing scenarios. Today, we will talk about the technical details of Go language template rendering in detail.

Mechanism of operation

The rendering technique for templates is essentially the same, in one sentence it is a combination of string templates and structured data, in more detail it is the application of a defined template to structured data, using annotation syntax to reference elements in the data structure (e.g. specific fields in Struct, keys in Map) and display their values. The template traverses the data structure during execution and sets the current cursor (`. `` often indicates the current scope) to identify the element at the current position.

Similar to template engines such as jinja in Python and jade in NodeJS, the Go language template engine operates on a similar mechanism.

  1. create template objects
  2. parse the template string
  3. load data to render the template

template

Go Template Rendering Core Package

The Go language provides two standard libraries to handle template rendering text/template and html/template, which have almost identical interfaces but handle different template data. One of them, text/template, is used to handle template rendering of plain text, while html/template is specifically used to render formatted HTML strings.

In the following example we use text/template to handle the rendering of normal text templates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "os"
    "text/template"
)

type Student struct {
    ID      uint
    Name    string
}

func main() {
    stu := Student{0, "jason"}
    tmpl, err := template.New("test").Parse("The name for student {{.ID}} is {{.Name}}")
    if err != nil { panic(err) }
    err = tmpl.Execute(os.Stdout, stu)
    if err != nil { panic(err) }
}

Line 4 of the above code introduces text/template to handle normal text template rendering, line 14 defines a template object test to parse the variable "The name for student {{.ID}} is {{.Name}}" template string, and line 16 uses the defined structured data to render the template to the standard output.

Note: The template data to be referenced must be exported, meaning that the corresponding fields must start with an uppercase letter, such as the ID and Name fields in the Student structure in the example.

Let’s look at another example of HTML string template rendering.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func templateHandler(w http.ResponseWriter, r *http.Request){
    tmpl := `<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Template Demo</title>
    </head>
    <body>
        {{ . }}
    </body>
</html>`
    
    t := template.New("hello.html")
    t, _ = t.Parse(tmpl)
    t.Execute(w, "Hello, Go Template!")
}

Note: It is not preferred to use single quotes for strings in Go; use double quotes or backquotes as needed. In addition, the Go language’s string types are inherently different from those of other languages; the String in Java, std::string in C++, and str in python3 are all just fixed-width character sequences, while the Go language’s string is a UTF-8 encoded sequence of variable-width characters, meaning that each character is represented by one or more bytes. string is a UTF-8 encoded sequence of variable-width characters, meaning that each character is represented by one or more bytes. String literals in Go are created using double quotes or backquotes ("`").

  • double quotes are used to create parsable string literals (escaping is supported, but they cannot be used to reference multiple lines).
  • backquotes are used to create native (raw) string literals, which may consist of multiple lines (no escape sequences are supported); native string literals are mostly used for writing multi-line messages, HTML, and regular expressions.

Local deployment execution.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ curl -i http://127.0.0.1:8080/
HTTP/1.1 200 OK
Date: Fri, 09 Dec 2016 09:04:36 GMT
Content-Length: 223
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Template Demo</title>
    </head>
    <body>
        Hello, Go Template!
    </body>
</html>

Go can not only parse template strings directly, but also use ParseFile to parse template files, or the standard processing flow: Create - Load - Render .

Template naming

The previous example template objects are named, either by displaying the naming when creating the template object, or by having the Go template named automatically. But how do you name a template when nested templates are involved, in which case there are multiple template files!

The Go template rendering package provides the ExecuteTemplate() method for rendering Go templates that execute the specified name. For example, when loading the hello template, you can specify layout.html.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    tmplstr := `{{ define "stu_info" }}
    The name for student {{.ID}} is {{.Name}}
    {{ end }}
    {{ define "stu_name" }}
    Student name is {{.Name}}
    {{ end }}
    `
    stu := Student{0, "jason"}
    tmpl, err := template.New("test").Parse(tmplstr)
    if err != nil { panic(err) }
    err = tmpl.ExecuteTemplate(os.Stdout, "stu_info", stu)
    if err != nil { panic(err) }
}

In the template string, the action define is used to define two named templates stu_info and stu_name. In this case, although the template object returned by the Parse() method contains both template names, the template executed by ExecuteTemplate() is still stu_info.

Not only can you define a template with define, but you can also introduce a defined template with template, similar to jinja’s include directive.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    tmplstr := `{{ define "stu_name" }}
    Student name is {{.Name}}
    {{ end }}
    {{ define "stu_info" }}
    {{ template "stu_name" . }}
    {{ end }}
    `
    stu := Student{0, "jason"}
    tmpl, err := template.New("test").Parse(tmplstr)
    if err != nil { panic(err) }
    err = tmpl.ExecuteTemplate(os.Stdout, "stu_info", stu)
    if err != nil { panic(err) }
}

In the above example we have introduced the stu_name template in the stu_info template using template and passed the data for the current scope of the stu_name template (. ), the third parameter is optional, if it is empty, it means that the data passed to the nested template is nil.

In a nutshell, the template object is created and multiple template files are loaded, the base template (stu_info) is specified when the template file is executed, and other named templates can be introduced in the base template. The keywords in double brackets such as define and template here are actually Go language template directives.

Template directive

From the previous introduction, we know that a template is actually a text or file that contains one or more {{ }} template strings enclosed by double brackets. Most template strings are simply printed at literal value, but if the template string contains an instruction (Action) it will trigger other behavior. Each directive contains an expression written in the template language. A directive is short but can output complex print values, and the template language includes many features such as control flow if-else statements and range loops by selecting structure members, calling functions or methods, expressions, and other instantiation templates.

In summary, Go language template rendering instructions are used to dynamically execute some form of logic and display data, and are broadly classified into the following categories.

  • conditional statements
  • iterations
  • wrapping
  • references

We saw the use of define and template in the previous example, here’s a look at how the other template directives are used.

Conditional judgments

The syntax of conditional judgments is simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{if pipeline}} T1 {{end}}
    If the value of the pipeline is empty, no output is generated;
    otherwise, T1 is executed. The empty values are false, 0, any
    nil pointer or interface value, and any array, slice, map, or
    string of length zero.
    Dot is unaffected.

{{if pipeline}} T1 {{else}} T0 {{end}}
    If the value of the pipeline is empty, T0 is executed;
    otherwise, T1 is executed. Dot is unaffected.

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
    To simplify the appearance of if-else chains, the else action
    of an if may include another if directly; the effect is exactly
    the same as writing
        {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

Take a simple example.

 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
func test1(a int) bool {
    if a == 3 {
        return true;
    }
    return false;
}

func main() {
    t := template.New("test");
    t.Funcs(template.FuncMap{"test1": test1});
    // {{if expression}}{{else if}}{{else}}{{end}}
    // if can be followed by a conditional expression, which can be a string or a boolean variable
    // Note that == cannot be used directly after if to determine
    t, _ = t.Parse(`
{{if 1}}
    it's true
{{else}}
    it's false
{{end}}
 
{{$a := 4}}
{{if $a|test1}}
    $a=3
{{else}}
    $a!=3
{{end}}
`);
    t.Execute(os.Stdout, nil);
    fmt.Println();

Iteration

For some serially tired data structures, such as Slice and Map, you can use the iterate instruction to iterate through the individual values, similar to iteration in Go itself, using range for processing.

Detailed specifications are as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{{range pipeline}} T1 {{end}}
    The value of the pipeline must be an array, slice, map, or channel.
    If the value of the pipeline has length zero, nothing is output;
    otherwise, dot is set to the successive elements of the array,
    slice, or map and T1 is executed. If the value is a map and the
    keys are of basic type with a defined order ("comparable"), the
    elements will be visited in sorted key order.

{{range pipeline}} T1 {{else}} T0 {{end}}
    The value of the pipeline must be an array, slice, map, or channel.
    If the value of the pipeline has length zero, dot is unaffected and
    T0 is executed; otherwise, dot is set to the successive elements
    of the array, slice, or map and T1 is executed.
1
2
3
4
5
{{ range . }}
    <li>{{ . }}</li>
{{ else }}
 empty
{{ end }}

When the object of range is empty, then the logic specified in the else branch is executed.

with wrapping

The with language opens up a contextual environment in Python. For the Go template, the with statement is similar in that it means that it creates a closed scope within which you can use . to get the arguments specified by the with directive, independent of the scope outside, and only with respect to the arguments of with.

Detailed specifications are as follows.

1
2
3
4
5
6
7
8
9
{{with pipeline}} T1 {{end}}
    If the value of the pipeline is empty, no output is generated;
    otherwise, dot is set to the value of the pipeline and T1 is
    executed.

{{with pipeline}} T1 {{else}} T0 {{end}}
    If the value of the pipeline is empty, dot is unaffected and T0
    is executed; otherwise, dot is set to the value of the pipeline
    and T1 is executed.

As an example.

1
2
3
{{ with arg }}
    .
{{ end }}

Inside the with instruction above, the . represents the newly opened scope, not the scope outside the with directive. The . in the with instruction and the . outside it are two unrelated objects. The with instruction can also have else, where the . is the same as the . is the same as ... outside of with; after all, only the with instruction has a closed context.

1
2
3
4
5
{{ with ""}}
 Now the dot is set to {{ . }}
{{ else }}
 {{ . }}
{{ end }}

Nested templates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func test1() string {
    return "test1";
}

func main() {
    t := template.New("test");
    t.Funcs(template.FuncMap{"test1": test1});
    // {{ define "templateName" }}Template Content{{end}} --> define the template
    // {{ template "templateName" }} --> reference the template
    // {{ template "templateName" function }} --> Pass the return value of function to {{.}}
    t, _ = t.Parse(`
{{define "tp1"}} template one {{end}}
{{define "tp2"}} template two {{.}} {{end}}
{{define "tp3"}} {{template "tp1"}} {{template "tp2"}} {{end}}
{{template "tp1"}}
{{template "tp2" test1}}
{{template "tp3" test1}}
`);
    t.Execute(os.Stdout, nil);
    fmt.Println();
}

Parameters and pipes

We mentioned earlier that the template and include template directives can have an optional third argument to pass data to the inner nested template. Among other things, the template argument can be a basic data type in Go, such as a number, a boolean, a string, an array slice, or a structure. Setting variables in a template can be done using: $variable := value.

Another feature of the Go language is the pipeline function for templates. Developers familiar with django and jinja should be familiar with this technique, which enables some simple formatting of templates by defining function filters, and through the pipeline philosophy, such processing can be linked together.

1
{{ 3.1415926 | printf "%.3f" }}

Functions

Since pipelining can be a filter in a template, Go template rendering supports custom functions to extend the functionality of the template, in addition to the built-in functions.

Defining a function in Go template rendering is a two-step process.

  1. create a map of type FuncMap, where key is the name of the template function and value is the definition of its function
  2. inject FuncMap into the template
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func fdate(t time.Time) {
    return t.Format(time.ANSIC)
}

func main() {
    funcMap := template.FuncMap{"fd": fdate}
    tmpl := template.New("test").Funcs(funcMap)
    tmpl = template.Must(t.Parse(`test data: {{ . | fd }}`))
    tmpl.ExecuteTemplate(os.Stdout, "test", time.Now())
}

You can use a pipeline syntax like {{ . | fd }}, but it is also possible to use the normal function call form {{ fd . }} can also serve the same purpose.

Summary

This note explores the basic syntax of Go template technology through examples, including the three steps of template rendering: Create - Load - Render, and how to use the common template tag directive in Go templates to implement complex data presentation logic. To summarize, Go language templating is an annotation syntax that references elements in a data structure and applies them to a defined template. go templating is often used to handle things like text transformations that insert specific data, and while it is not as flexible as regular expressions, it is faster to render than regular expressions and easier to use.