Learn how to use Pongo2 templates in Go. Includes examples of template rendering and customization.
last modified April 11, 2024
In this article we show how to work with templates in Golang with pongo2 template engine.
A template engine is a library designed to combine templates with a data to produce documents. Template engines are used to generate large amounts of emails, in source code preprocessing, or to produce dynamic HTML pages.
A template consists of static data and dynamic regions. The dynamic regions are later replaced with data. The rendering function later combines the templates with data. A template engine is used to combine templates with a data model to produce documents.
The pongo2 library is a Go template engine inspired by Django’s template engine.
The pongo2 uses various delimiters in template string:
{% %} - statements
{{ }} - expressions to print to the template output
{# #} - comments which are not included in the template output
Templates can be read from strings with pongo2.FromString, files with pongo2.FromFile, or bytes with pongo2.FromBytes.
The documents are rendered with Execute, ExecuteWriter, or ExecuteBytes functions. These functions accept a Context, which provides constants, variables, instances or functions to a template.
The pongo2.FromString reads a template from a string.
main.go
package main
import ( “fmt” “log”
"github.com/flosch/pongo2/v5"
)
func main() {
tpl, err := pongo2.FromString("Hello {{ name }}!")
if err != nil {
log.Fatal(err)
}
res, err := tpl.Execute(pongo2.Context{"name": "John Doe"})
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
The example produces a simple text message.
tpl, err := pongo2.FromString(“Hello {{ name }}!”)
The variable to print is placed within the {{ }} brackets.
res, err := tpl.Execute(pongo2.Context{“name”: “John Doe”})
We render the final string with Execute. In the context, we pass a value for the name variable.
$ go run main.go Hello John Doe!
main.go
package main
import (
"fmt"
"log"
"github.com/flosch/pongo2/v5"
)
func main() {
tpl, err := pongo2.FromString("{{ name }} is a {{ occupation }}")
if err != nil {
log.Fatal(err)
}
name, occupation := "John Doe", "gardener"
ctx := pongo2.Context{"name": name, "occupation": occupation}
res, err := tpl.Execute(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
In this example, we pass two variables in the context.
$ go run main.go John Doe is a gardener
With the pongo2.FromFile function, we read the template from a file.
message.tpl
{{ name }} is a {{ occupation }}
This is the template file.
main.go
package main
import ( “fmt” “log”
"github.com/flosch/pongo2/v5"
)
func main() {
tpl, err := pongo2.FromFile("message.tpl")
if err != nil {
log.Fatal(err)
}
name, occupation := "John Doe", "gardener"
ctx := pongo2.Context{"name": name, "occupation": occupation}
res, err := tpl.Execute(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
The example produces a simple message, while reading the template from a file.
The for directive is used to iterate over a data collection in a template.
words.tpl
{% for word in words -%} {{ word }} {% endfor %}
In the template, we use the for directive to go through the elements of the words data structure. The - character strips whitespace characters.
main.go
package main
import ( “fmt” “log”
"github.com/flosch/pongo2/v5"
)
func main() {
tpl, err := pongo2.FromFile("words.tpl")
if err != nil {
log.Fatal(err)
}
words := []string{"sky", "blue", "storm", "nice", "barrack", "stone"}
ctx := pongo2.Context{"words": words}
res, err := tpl.Execute(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
In the program, we pass a slice of words to the tempate engine. We get a list of words as the output.
$ go run main.go sky blue storm nice barrack stone
A filter can be applied to data to modify them. Filters are applied after the | character.
words.tpl
{% for word in words -%} {{ word }} has {{ word | length }} characters {% endfor %}
The length filter returns the size of the string.
main.go
package main
import ( “fmt” “log”
"github.com/flosch/pongo2/v5"
)
func main() {
tpl, err := pongo2.FromFile("words.tpl")
if err != nil {
log.Fatal(err)
}
words := []string{"sky", "blue", "storm", "nice", "barrack", "stone"}
ctx := pongo2.Context{"words": words}
res, err := tpl.Execute(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
In the program, we pass a slice of words to the template. We print each word and its size.
$ go run main.go sky has 3 characters blue has 4 characters storm has 5 characters nice has 4 characters barrack has 7 characters stone has 5 characters
Conditions can be created with if/endif directives.
todos.tpl
{% for todo in todos -%} {% if todo.Done %} {{- todo.Title -}} {% endif %} {% endfor %}
In the template file, we use the if directive to output only tasks that are finished.
main.go
package main
import ( “fmt” “log”
"github.com/flosch/pongo2/v5"
)
type Todo struct { Title string Done bool }
type Data struct { Todos []Todo }
func main() {
tpl, err := pongo2.FromFile("todos.tpl")
if err != nil {
log.Fatal(err)
}
todos := []Todo{
{Title: "Task 1", Done: false},
{Title: "Task 2", Done: true},
{Title: "Task 3", Done: true},
{Title: "Task 4", Done: false},
{Title: "Task 5", Done: true},
}
ctx := pongo2.Context{"todos": todos}
res, err := tpl.Execute(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
}
We generate on output from a slice of todos. In the output we include only finished tasks.
In the next example, we use templates in a server application.
users.html
<!DOCTYPE html> <html lang=“en”> <head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <title>Users</title> </head>
<body> <table> <thead> <tr> <th>Name</th> <th>Occupation</th> </tr> </thead> <tbody> {% for user in users %} <tr> <td>{{ user.Name }} </td> <td>{{ user.Occupation }}</td> </tr> {% endfor %} </tbody> </table> </body> </html>
The output is an HTML file. The users are displayed in an HTML table.
main.go
package main
import ( “net/http”
"github.com/flosch/pongo2/v5"
)
type User struct { Name string Occupation string }
var tpl = pongo2.Must(pongo2.FromFile(“users.html”))
func usersHandler(w http.ResponseWriter, r *http.Request) {
users := []User{
{Name: "John Doe", Occupation: "gardener"},
{Name: "Roger Roe", Occupation: "driver"},
{Name: "Peter Smith", Occupation: "teacher"},
}
err := tpl.ExecuteWriter(pongo2.Context{"users": users}, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/users", usersHandler)
http.ListenAndServe(":8080", nil)
}
The web server returns an HTML page with a table of users for the /users URL path.
var tpl = pongo2.Must(pongo2.FromFile(“index.html”))
The pongo2.Must is a helper function which pre-compiles the templates at application startup.
err := tpl.ExecuteWriter(pongo2.Context{“users”: users}, w)
The ExecuteWriter renders the template with the given context and writes the output to the response writer on success. Nothing is written on error; instead the error is being returned.
In this article we have created dynamic documents using third-party pongo2 templating engine.
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all Go tutorials.