Embedding static assets in a Go project

September 7, 2015

I wanted to build a small self-contained web application in Go, at the opposite of a usual webapp where the assets will be served separately via a CDN or HTTP server like Nginx. But if performance doesn't matter or it's aimed for a small traffic then having a self-contained application makes it easier to deploy and to distribute as it's simply a binary to run.

There are few packages which allow to embed assets into a Go application:

I'm not going to go into the details of each library but, I preferred the approach of bindata, simpler to get started, and actively maintained. Also, the step to do the embedding of assets is very explicit in the code.

First, you will need to have a Go workspace setup. If you are unfamilliar with Go then you can refer to the documentation or my previous post on the topic.

Once you got a Go workspace and project setup then let's create an index.html inside a frontend/ directory within your project. A simple 'Hello World' will do:

<!-- frontend/index.html -->
<html>
  <body>
    Hello, World!
  </body>
</html>

Now that we have a project setup and a static asset to test, let's install bindata via:

go get -u github.com/jteeuwen/go-bindata/...

We are good to start the webapp backend code. Create main.go file and copy the following code:

package main

import (
    "bytes"
    "io"
  "net/http"
)

//go:generate go-bindata -prefix "frontend/" -pkg main -o bindata.go frontend/...

func static_handler(rw http.ResponseWriter, req *http.Request) {
  var path string = req.URL.Path
  if path == "" {
    path = "index.html"
  }
  if bs, err := Asset(path); err != nil {
    rw.WriteHeader(http.StatusNotFound)
  } else {
    var reader = bytes.NewBuffer(bs)
    io.Copy(rw, reader)
  }
}

func main() {
  http.Handle("/", http.StripPrefix("/", http.HandlerFunc(static_handler)))
    http.ListenAndServe(":3000", nil)
}

The important line in this code is:

//go:generate go-bindata -prefix "frontend/" -pkg main -o bindata.go frontend/...

The line above allows us to run the go-bindata command line when we are calling go generate. From Go 1.4, it's allowed to run custom commands during a generate step. It's just a matter of adding //go:generate command argument... inside your go file.

The command line go-bindata have multiple options so check the documentation on how to use it. In our case, we are telling bindata few things:

  • -prefix "frontend/" to define a part of a path name to be stripped off
  • -pkg main to define the package name used in the generated code
  • -o bindata.go to define the name of the generated file

Once the go generate command has run, you should see a file called bindata.go being generated. Your project's structure should look like this:

.
│ 
├── bindata.go (auto-generated file)
├── frontend
│   └── index.html
└── main.go

The logic to serve the static asset is in the static_handler function, which receive a request and check if the path matches an asset. The check is done using the function Asset automatically exported by bindata.go. If the asset doesn't exist then we return a 404 otherwise we return the content of the asset.

The rest of the code is to create the web application and associate our static_handler with a pattern / to match all entering requests. If you've got problems understanding this code then check the http package documentation.

As a quick reminder about how Go handle packages, all identifiers will be automatically exported to other packages of the same name if the first letter of the identifier name starts with an uppercase letter.

Base on this rule, bindata.go file expose an Asset function to the main package. This loads and returns the asset for a given name. It returns an error if the asset could not be found or could not be loaded.

This post doesn't go into advanced details but I may write other posts as I'm progressing in my application.