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:
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.
The comment directive must be placed immediately above the variable in which you want to store the embedded files.
The directive has the general format
go:embed <paths>
, and it’s OK to specify multiple paths in one directive (like we have in the code above). The paths should be relative to the source code file containing the directive. So in our case,go:embed "static" "html"
embeds the directoriesui/static
andui/html
from our project.You can only use the
go:embed
directive on global variables at package level, not within functions or methods. If you try to use it within a function or method, you’ll get the error"go:embed cannot apply to var inside func"
at compile time.Paths cannot not contain
.
or..
elements, nor may they begin or end with a/
. This essentially restricts you to only embedding files that are contained in the same directory (or a subdirectory) as the source code which has thego:embed
directive.If a path is to a directory, then all files in that directory are recursively embedded, except for files with names that begin with
.
or_
. If you want to include these files you should use theall:
prefix, likego:embed "all:static"
.The path separator should always be a forward slash, even on Windows machines.
The embedded file system is always rooted in the directory which contains the
go:embed
directive. So, in the example above, ourFiles
variable contains anembed.FS
embedded filesystem and the root of that filesystem is ourui
directory.
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:
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.

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.

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:
fs.Glob()
returns a slice of filepaths matching a glob pattern. It’s effectively the same as thefilepath.Glob()
function that we used earlier in the book, except that it works on embedded filesystems.Template.ParseFS()
can be used to parse the HTML templates from an embedded filesystem into a template set. This is effectively a replacement for both theTemplate.ParseFiles()
andTemplate.ParseGlob()
methods that we used earlier.Template.ParseFiles()
is also a variadic function, which allows you to parse multiple templates in a single call toParseFiles()
.
Let’s put these to use in our cmd/web/templates.go
file:
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.
