Embedding Assets with embed

//go:embed copies templates and assets into the compiled binary, so a deploy is one file with nothing left to copy alongside it.

Syntax//go:embed static templates/*.html

A compiled Go program is a single executable file. The templates, CSS and images it serves are not inside it. By default they are read from disk while the program runs, which means two things to deploy and a directory layout to get right. Miss one file and the program only fails at the first request. The //go:embed directive copies files into the executable at build time, so there is one file to ship.

How to write it

Put a comment of the form //go:embed pattern directly above a package-level variable. Only blank lines and other comments may sit between them. The variable's type decides what you get. string or []byte holds one file. embed.FS holds many, and is a read-only file system you can hand to anything that accepts an fs.FS, such as template.ParseFS or http.FileServerFS. Embedding arrived in Go 1.16.

Both import spellings exist for a reason. With embed.FS you write import "embed" because your code names the type. With a string or []byte variable the code never mentions the package, so you write import _ "embed". The underscore means: load this package, I will not refer to it.

Pattern rules

Patterns are relative to the directory holding the source file. They cannot contain .., cannot leave the module, and do not follow symlinks. A pattern matching nothing is a build error. Names beginning with a dot or an underscore are skipped unless the pattern starts with all: (Go 1.18).

Paths keep the directory

//go:embed static gives you static/style.css, not style.css. fs.Sub(assets, "static") returns a file system rooted inside that directory, so URLs do not repeat the name.

Example

Example · go
package main

import (
	"embed"
	"fmt"
	"io/fs"
	"log"
	"net/http"
	"net/http/httptest"
)

// Next to this file, create a directory "static" holding one file,
// static/style.css, containing exactly: body{margin:0}
//
//go:embed static
var staticFiles embed.FS

func main() {
	// Paths inside the embedded file system keep the directory name.
	css, err := staticFiles.ReadFile("static/style.css")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("read: %s\n", css)

	// fs.Sub drops the "static/" prefix so the URL does not repeat it.
	assets, err := fs.Sub(staticFiles, "static")
	if err != nil {
		log.Fatal(err)
	}
	rec := httptest.NewRecorder()
	http.FileServerFS(assets).ServeHTTP(rec, httptest.NewRequest("GET", "/style.css", nil))
	fmt.Printf("served: %d %s\n", rec.Code, rec.Body.String())
}

// Output:
// read: body{margin:0}
// served: 200 body{margin:0}

When to use it

  • Ship HTML templates inside the binary so the container image needs no extra files.
  • Embed SQL migration files so the program can create its own tables on first run.
  • Serve a compiled React or Vue build from the same binary as the API.
  • Carry a default configuration file or a lookup table the program needs at start-up.

More examples

Templates parsed from the binary

This is the usual shape of a server-rendered Go application: one embed directive, one Must(ParseFS(...)) at package level, and handlers that call ExecuteTemplate. The glob appears twice on purpose, once for the compiler and once for the parser. Because the files are a build-time snapshot, editing a template means rebuilding before you see the change.

Example · go
package main

import (
	"embed"
	"html/template"
	"log"
	"os"
)

//go:embed templates/*.html
var templateFiles embed.FS

// Parsed once at start-up. ParseFS (Go 1.16) takes the patterns a second
// time, matched against the embedded file system rather than the disk.
// template.Must panics on a parse error, which is what you want at start-up.
var pages = template.Must(template.ParseFS(templateFiles, "templates/*.html"))

func main() {
	// A template's name is the base name of its file, not the full path.
	if err := pages.ExecuteTemplate(os.Stdout, "home.html", "Ada"); err != nil {
		log.Fatal(err)
	}
}

// With templates/home.html containing: <h1>Hello {{.}}</h1>
//
// Output:
// <h1>Hello Ada</h1>

One file into a string

Reach for a string or []byte variable when there is exactly one file: a SQL schema, a default config, a licence text. Note the blank import. Without it the build fails, because //go:embed requires the embed package to be imported even when your code never writes the word embed.

Example · go
package main

import (
	"database/sql"
	_ "embed" // the directive needs this package; the code never names it
)

// A single pattern and a string variable: the file's text, nothing else.
//
//go:embed schema.sql
var schema string

func createTables(db *sql.DB) error {
	// schema is an ordinary string, so it goes straight to the database.
	_, err := db.Exec(schema)
	return err
}

A built frontend with all:

Use this when one binary serves both an API and a compiled single-page app. The frontend build must run before go build, because the compiler reads whatever is in dist at that moment; in CI that means the npm build step comes first. A missing dist directory is a build error rather than a run-time surprise, which is the behaviour you want.

Example · go
package ui

import (
	"embed"
	"io/fs"
	"net/http"
)

//go:embed all:dist
var frontend embed.FS

// Handler serves the built frontend from inside the binary. Without the
// all: prefix (Go 1.18) the build would skip dist/_next and dist/.vite,
// and the page would load with no JavaScript and no error anywhere.
func Handler() (http.Handler, error) {
	assets, err := fs.Sub(frontend, "dist")
	if err != nil {
		return nil, err
	}
	return http.FileServerFS(assets), nil
}

Discussion

  • Be the first to comment on this lesson.