Let's Go Optional Go features › Using embedded files
Previous · Contents · Next
Chapter 13.1.

Using embedded files

Go provides an embed package, which makes it possible to embed external files into your Go program itself.

This feature is really nice because it makes it possible to create (and subsequently, distribute) Go programs that are completely self-contained and have everything that they need to run as part of the binary executable.

To illustrate how to use the embed package, we’ll update our application to embed and use the files in our existing ui directory (which contains our static CSS/JavaScript/image files and the HTML templates).

If you’d like to follow along, first create a new ui/efs.go file:

$ touch ui/efs.go

And then add the following code:

File: ui/efs.go
package ui

import (
    "embed"
)

//go:embed "html" "static"
var Files embed.FS

The important line here is //go:embed "html" "static".

This looks like a comment, but it is actually a special comment directive. When our application is compiled, this comment directive instructs Go to store the files from our ui/html and ui/static folders in an embed.FS embedded filesystem referenced by the global variable Files.

There are a few important details about this which we need to explain.

Using the static files

Let’s switch up our application so that it serves our static CSS, JavaScript and image files from the embedded file system — instead of reading them from the disk at runtime.

Open your cmd/web/routes.go file and update it as follows:

File: cmd/web/routes.go
package main

import (
    "net/http"

    "snippetbox.alexedwards.net/ui" // New import

    "github.com/julienschmidt/httprouter"
    "github.com/justinas/alice"
)

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        app.notFound(w)
    })

    // Take the ui.Files embedded filesystem and convert it to a http.FS type so
    // that it satisfies the http.FileSystem interface. We then pass that to the
    // http.FileServer() function to create the file server handler.
    fileServer := http.FileServer(http.FS(ui.Files))

    // Our static files are contained in the "static" folder of the ui.Files
    // embedded filesystem. So, for example, our CSS stylesheet is located at
    // "static/css/main.css". This means that we now longer need to strip the
    // prefix from the request URL -- any requests that start with /static/ can
    // just be passed directly to the file server and the corresponding static
    // file will be served (so long as it exists).
    router.Handler(http.MethodGet, "/static/*filepath", fileServer)

    dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)

    router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
    router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
    router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
    router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
    router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
    router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))

    protected := dynamic.Append(app.requireAuthentication)

    router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
    router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
    router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))

    standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
    return standard.Then(router)
}

If you save the files and then restart the application, you should find that everything compiles and runs correctly. When you visit https://localhost:4000 in your browser, the static files should be served from the embedded filesystem and everything should look normal.

13.01-01.png

If you want, you can also navigate directly to the static files to check that they are still available. For example, visiting https://localhost:4000/static/css/main.css should display the CSS stylesheet for the webpage from the embedded filesystem.

13.01-02.png

Embedding HTML templates

Next let’s update the cmd/web/templates.go file so that our template cache uses the embedded HTML template files from ui.Files, instead of the ones on disk.

To help us with this, we’ll need to leverage a couple of the special features that Go has for working with embedded filesystems:

Let’s put these to use in our cmd/web/templates.go file:

File: cmd/web/templates.go
package main

import (
    "html/template"
    "io/fs" // New import
    "path/filepath"
    "time"

    "snippetbox.alexedwards.net/internal/models"
    "snippetbox.alexedwards.net/ui" // New import
)

...

func newTemplateCache() (map[string]*template.Template, error) {
    cache := map[string]*template.Template{}

    // Use fs.Glob() to get a slice of all filepaths in the ui.Files embedded
    // filesystem which match the pattern 'html/pages/*.tmpl'. This essentially
    // gives us a slice of all the 'page' templates for the application, just
    // like before.
    pages, err := fs.Glob(ui.Files, "html/pages/*.tmpl")
    if err != nil {
        return nil, err
    }

    for _, page := range pages {
        name := filepath.Base(page)

        // Create a slice containing the filepath patterns for the templates we
        // want to parse.
        patterns := []string{
            "html/base.tmpl",
            "html/partials/*.tmpl",
            page,
        }

        // Use ParseFS() instead of ParseFiles() to parse the template files 
        // from the ui.Files embedded filesystem.
        ts, err := template.New(name).Funcs(functions).ParseFS(ui.Files, patterns...)
        if err != nil {
            return nil, err
        }

        cache[name] = ts
    }

    return cache, nil
}

Now that this is done, when our application is built into a binary it will contain all the UI files that it needs to run.

You can try this out quickly by building an executable binary in your /tmp directory, copying over the TLS certificates and running the binary. Like so:

$ go build -o /tmp/web ./cmd/web/
$ cp -r ./tls /tmp/
$ cd /tmp/
$ ./web 
INFO    2022/04/01 12:43:12 Starting server on :4000

And again, you should be able to visit https://localhost:4000 in your browser and everything should work correctly — despite the binary being in a location where it does not have access to the original UI files on disk.

13.01-01.png