Pongo with Echo or net/http

January 15, 2017

It’s been a while since I started web development with Go, and I’ve been enjoying it a lot. I’ve used mainly Echo and the well-known net/http library.

However, today I am not here to talk about web frameworks, but about a templating engine instead. I like the default html/template package, but I am not very fond of it when it comes to bigger projects. Some things are simply harder to accomplish (like template inheritance) than I am used to.

Aside from that, on certain projects I work with front-end developers who are rarely familiar with the syntax of the default templating library. And I’ve heard more than once that the documentation regarding this library is hard to digest. I can understand why, if you’re not accustomed to Go or it’s documentation.

However, there is an alternative, which both I and most of the people I work with very much prefer, it’s called pongo2. It is based on Django-template syntax, but nowadays a lot of programming languages have libraries supporting this syntax:

So let’s get started, shall we?

First of all, install pongo2: go get -u github.com/flosch/pongo2

The most important method to start rendering from files is pongo2.FromFile. It has expects one argument: a string, the name of the file you want to render.

It returns a *pongo2.Template and an error. On this *pongo2.Template we call ExecuteWriter and pass it a pongo2.Context, which is the data your template will be using, and an io.Writer.

Throughout my examples I will use this simple template:

<p>My name: {{ name }}</p>
<p>The answer: {{ answer() }}</p>
<p>
  Items:
  {% for item in items %}
  <li>{{ item }}</li>
  {% endfor %}
</p>

The result will always be:

<p>My name: Machiel</p>
<p>The answer: 42</p>
<p>
  Items:
  <li>Apple</li>
  <li>Pear</li>
  <li>Banana</li>
  <li>Pineapple</li>
</p>

A simple example using net/http

Assume the following code, using a simple HTTP server that listens on :1333:

package main

import (
	"log"
	"net/http"

	"github.com/flosch/pongo2"
)

func main() {
	http.HandleFunc("/", Index)
	http.ListenAndServe(":1333", nil)
}

func Index(w http.ResponseWriter, r *http.Request) {
	t, err := pongo2.FromFile("index.html")

	if err != nil {
		log.Printf("could not render: %s", err)
		return
	}

	data := pongo2.Context{
		"name": "Machiel",
		"answer": func() int {
			return 42
		},
		"items": []string{
			"Apple", "Pear", "Banana", "Pineapple",
		},
	}

	if err := t.ExecuteWriter(data, w); err != nil {
		log.Printf("could not execute: %s", err)
	}
}

The only place where something actually happens is the Index function. First we try to load the template from a file, next up we build our context (pongo2.Context). Under the hood it is nothing more than a map[string]interface{}, so you can pass in pretty much whatever you like: structs, interfaces, functions, you name it.

After that we use the ExecuteWriter function to write to the http.ResponseWriter. And that’s it, you’ve just used pongo2.

Better example

I generally like to declare a Renderer struct which handles the rendering for me, just passing the required dependencies at that moment. Then you just pass the Renderer to your handler functions, and call the Render function with the desired parameters, as shown in this example:

package main

import (
	"io"
	"log"
	"net/http"

	"github.com/flosch/pongo2"
)

type Renderer struct {
	Debug bool
}

func (r Renderer) Render(w io.Writer, name string, data pongo2.Context) error {
	var t *pongo2.Template
	var err error

	if r.Debug {
		t, err = pongo2.FromFile(name)
	} else {
		t, err = pongo2.FromCache(name)
	}

	// Add some static values
	data["version_number"] = "v0.0.1-beta"

	if err != nil {
		return err
	}

	return t.ExecuteWriter(data, w)
}

func main() {
	renderer := Renderer{
		Debug: false,
	}

	http.HandleFunc("/", Index(&renderer))
	http.ListenAndServe(":1333", nil)
}

func Index(renderer *Renderer) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		data := pongo2.Context{
			"name": "Machiel",
			"answer": func() int {
				return 42
			},
			"items": []string{
				"Apple", "Pear", "Banana", "Pineapple",
			},
		}

		if err := renderer.Render(w, "index.html", data); err != nil {
			log.Printf("could not execute: %s", err)
		}
	}
}

This is a very trivial example, but you could do crazy here and pass things like the base directory for your templates to the Renderer.

Example with echo

As for echo, the code looks quite the same. At the time of writing I am using echo v3, the way of using a Renderer in previous versions of echo is slightly different (as in, one or two lines of code). If you’re unable to figure out how to use it though, let me know.

package main

import (
	"errors"
	"io"
	"net/http"

	"github.com/labstack/echo"
	"github.com/flosch/pongo2"
)

type Renderer struct {
	Debug bool
}

func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {

	var ctx pongo2.Context

	if data != nil {
		var ok bool
		ctx, ok = data.(pongo2.Context)

		if !ok {
			return errors.New("no pongo2.Context data was passed...")
		}
	}

	var t *pongo2.Template
	var err error

	if r.Debug {
		t, err = pongo2.FromFile(name)
	} else {
		t, err = pongo2.FromCache(name)
	}

	// Add some static values
	ctx["version_number"] = "v0.0.1-beta"

	if err != nil {
		return err
	}

	return t.ExecuteWriter(ctx, w)
}

func main() {
	renderer := Renderer{
		Debug: false,
	}

	e := echo.New()
	e.Renderer = renderer
	e.GET("/", Index)
	e.Start(":1313")
}

func Index(c echo.Context) error {
	data := pongo2.Context{
		"name": "Machiel",
		"answer": func() int {
			return 42
		},
		"items": []string{
			"Apple", "Pear", "Banana", "Pineapple",
		},
	}

	return c.Render(http.StatusOK, "index.html", data)
}

I hope this was a good (albeit short) introduction to pongo2, but it should at least be enough to get up and running. I would say, give it a shot :).


Get Machiel Mail in your inbox!

Of course I have a newsletter, all the cool kids have them! Sign up if you want to get notified of the occasional blog post I write. They will be mostly about Go, or simply about projects I am working on. And it might just turn out that they are interesting!

And if you're wondering, will he send sp... No, I will never send you any spam!

comments powered by Disqus