ozkar's blog

Using go-bindata to embed views into the binary

February 06, 2018 | 6 Minute Read

This is a quick tutorial showing how to use go-bindata to embed views into your binary so your appplication deployment is just that binary.

Normal Approach:

In a normal application we read the views using the html/template package, like so:

t, _ := template.ParseFiles("views/layout.html.tmpl", "views/body.html.tmpl")

Then we simply do

t.Execute(w, data)

where w is our http.ResponseWriter

Using go-bindata:

The first thing we need to do is package the views into a go file, this is done by running the next command:

go-bindata views/...

You can even add multiple folders, strip prefixes and a bunch of goodies, but thats better explained here

This will create a file called bindata.go, i recomend changing the package name from main into something like assets or bindata that way you can share your bin data trough all the packages your application has.

If you browse trough the file you would see it looks something like this:

// Code generated by go-bindata.
// sources:
// assets/css/bulma.min.css
// assets/css/font-awesome.min.css
// assets/fonts/fontawesome-webfont.eot
// assets/fonts/fontawesome-webfont.ttf
// assets/fonts/fontawesome-webfont.woff
// assets/fonts/fontawesome-webfont.woff2
// views/bodyt.html.tmpl
// views/layout.html.tmpl
// DO NOT EDIT!

package bindata

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

var _assetsCssBulmaMinCss = []byte("HUGE HEX FILE HERE")

func assetsCssBulmaMinCssBytes() ([]byte, error) {
	return bindataRead(
		_assetsCssBulmaMinCss,
		"assets/css/bulma.min.css",
	)
}

func assetsCssBulmaMinCss() (*asset, error) {
	bytes, err := assetsCssBulmaMinCssBytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "assets/css/bulma.min.css", size: 137589, mode: os.FileMode(438), modTime: time.Unix(1517967570, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}
//Lots more code...

Towards the end of a file theres a simple map containing the value of the files in hex and using the path to the file as key:

// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
	"assets/css/bulma.min.css": assetsCssBulmaMinCss,
	"assets/css/font-awesome.min.css": assetsCssFontAwesomeMinCss,
	"assets/fonts/fontawesome-webfont.eot": assetsFontsFontawesomeWebfontEot,
	"assets/fonts/fontawesome-webfont.ttf": assetsFontsFontawesomeWebfontTtf,
	"assets/fonts/fontawesome-webfont.woff": assetsFontsFontawesomeWebfontWoff,
	"assets/fonts/fontawesome-webfont.woff2": assetsFontsFontawesomeWebfontWoff2,	
	"views/layout.html.tmpl": viewsLayoutHtmlTmpl,
	"views/body.html.tmpl": viewsBodyHtmlTmpl,
}

and the Asset function, which is a wrapper to accesing the values of the map.

To access a file []byte you only need to call:

bytes, err := bindata.Asset("views/layout.html.tmp")

(in my case because i changed the package from main to bindata) and it will, unsurprisingly give us the value of the key for the _bindata variable, it will return and error if anything goes wrong.

Now to execute the template we must first convert it into string like this:

stringFile := string(bytes)

and then we can feed it into the html/template package like a string.

Then replace the function template.ParseFile for

t, := template.New("cacheNameHere").Parse(stringFile)

after that its regular business serving with t.Execute(w).

Theres only one problem to this, the template.ParseFiles function call is variadic, and you can supply various templates at once like when we used in the previous example:

t, _ := template.ParseFiles("views/layout.html.tmpl", "views/body.html.tmpl")

where we supply a layout.

To fix these we must understand what template.ParseFiles does first:

It basically iterates trough the filenames, and instanciate a template with t := template.New and subsequently runs t.Parse on each file after reading them with iotutil.ReadFile source here

So you can either:

Run subsequent Parse for each file:

  view, err := bindata.Asset("views/body.html.tmpl")
  layout, err := bindata.Asset("views/layout.html.tmpl")

  t := template.New("test")
  t, err := t.Parse(string(layout))
  t, err := t.Parse(string(view))

  t.Execute(w, data)

or simply concat the strings when parsing:

  view, err := bindata.Asset("views/body.html.tmpl")
  layout, err := bindata.Asset("views/layout.html.tmpl")

  t, err := template.New("test").Parse(string(layout) + string(view))
  t.Execute(w, data)

Conclusion:

go-bindata is a tool that can allow us to embed resources into our application binary to keep deployment as simple as posible.

Bonus:

Embed static assets, and serve them with an asset handler:

First the asset handler:

package handlers

import (
	"strings"
	"net/http"

	"<yourproject>/bindata"
)

type AssetsHandler struct{}

func NewAssetsHandler() *AssetsHandler {
	return &AssetsHandler{}
}

func (a *AssetsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	request := strings.TrimPrefix(r.URL.Path, "/")
	file, err := bindata.Asset(request)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	w.Write(file)
}

and then simply add the route to your router:

r.PathPrefix("/assets").Handler(handlers.NewAssetsHandler()

this code is using gorilla/mux

also, for this to work you would need to include an assets directory along with the views directory for go-bindata like so:

go-bindata views/... assets/...