How to use embed package

Use the `embed` package to compile static files directly into your Go binary at build time, eliminating the need for external file dependencies or runtime file system access.

The deployment headache

You deploy your Go binary to a production server. The code runs. The database connects. Then the server crashes because it can't find templates/index.html. You forgot to copy the assets, or the working directory is different inside the container. You end up wrestling with Docker volumes, relative paths that break when the binary moves, or scripts to sync files.

Go solves this by letting you bake files directly into the binary. The embed package turns your static assets into part of the executable. No external files. No path hunting. Just one file that contains everything. When you ship the binary, the assets travel with it.

Baking files into the binary

The embed package lives in the standard library. It uses a special compiler directive, //go:embed, to tell the build tool to read specific files and embed them into the resulting binary. You declare a variable of type embed.FS. The compiler scans the files matching your pattern, reads their contents, and generates code that stores those contents inside the variable.

When your program runs, it accesses the data from memory, not the disk. The binary grows larger, but it becomes completely self-contained. The embedded filesystem implements the fs.FS interface, which means it works with standard library functions like http.FileServer and template.ParseFS. This follows the Go mantra: accept interfaces, return structs. Functions accept fs.FS, and embed.FS satisfies that interface.

Embed turns your binary into a portable archive. Ship one file, run anywhere.

Minimal example: one file

Here's the simplest case: embed one file and read it back. The directive must sit immediately above the variable declaration with no blank lines.

package main

import (
	"embed"
	"fmt"
)

//go:embed config.json
// The directive must be a comment directly preceding the variable.
// The compiler reads config.json and binds it to this variable.
var configFile embed.FS

func main() {
	// ReadFile takes the path relative to the embed pattern.
	// Since the pattern is a single file, the path is just the filename.
	data, err := configFile.ReadFile("config.json")
	if err != nil {
		panic(err)
	}
	// Convert bytes to string for display.
	fmt.Println(string(data))
}

The configFile variable holds an embed.FS. Even though you embedded a single file, the type is a filesystem. This allows the same API to work for single files and directories. The ReadFile method returns the bytes. If the file wasn't embedded, ReadFile returns an error.

The if err != nil check is verbose by design. The Go community accepts the boilerplate because it makes the unhappy path visible. Never ignore errors from file operations, even with embedded data.

How the compiler works

When you run go build, the compiler sees the //go:embed comment. It checks the filesystem for the files matching the pattern. If the files exist, the compiler reads them and generates initialization code that holds the bytes. The resulting binary contains those bytes.

If you run the binary on a machine that has no config.json file, it still works because the data is inside the executable. The ReadFile method returns the data from the embedded filesystem, which is an in-memory representation. You never touch the disk at runtime.

If a file is missing during the build, the compiler rejects the program with pattern config.json: no matching files found. This is a feature. It catches missing assets before deployment. A build failure is better than a runtime crash.

Serving a directory

Serving a directory requires handling paths carefully. The embedded filesystem preserves the directory structure from the pattern. If you embed static/*, the paths inside the filesystem start with static/. You need to strip that prefix to serve files from the root.

Here's how to serve a directory of assets via an HTTP server. The fs.Sub function creates a sub-filesystem rooted at a specific path, effectively removing the prefix.

package main

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

//go:embed static/*
// The wildcard matches all files and directories inside static/.
// The resulting embed.FS contains paths like "static/index.html".
var staticFiles embed.FS

func main() {
	// fs.Sub strips the "static/" prefix from paths.
	// Without this, users would need to request /static/index.html.
	// With fs.Sub, requests to /index.html work as expected.
	subFS, err := fs.Sub(staticFiles, "static")
	if err != nil {
		panic(err)
	}

	// http.FileServer expects an fs.FS interface.
	// embed.FS implements fs.FS, so it works directly.
	handler := http.FileServer(http.FS(subFS))

	// Serve the files on the root path.
	http.Handle("/", handler)
	http.ListenAndServe(":8080", nil)
}

The fs.Sub call is crucial. Without it, the file server expects paths like /static/index.html. With fs.Sub, the server treats static/ as the root, so /index.html resolves correctly. The http.FS wrapper converts the fs.FS to the older http.FileSystem interface, which http.FileServer requires.

Convention aside: gofmt is mandatory. Don't argue about indentation. Most editors run gofmt on save, and the Go toolchain expects standard formatting. The import block above is sorted by gofmt, with standard library packages first.

Working with templates

Embedding HTML templates is a common use case. The text/template package provides ParseFS, which parses templates from an fs.FS. This lets you load all templates at startup and reuse them.

Here's a handler that renders a template from the embedded filesystem. The template variable is package-level so it's parsed once.

package main

import (
	"embed"
	"html/template"
	"net/http"
)

//go:embed templates/*.html
// Matches all HTML files in the templates directory.
var templates embed.FS

// tmpl holds the parsed template set.
// Initialize this once at startup to avoid re-parsing on every request.
var tmpl *template.Template

func init() {
	// ParseFS takes the embedded FS and a pattern.
	// It parses all matching templates into a single template set.
	var err error
	tmpl, err = template.ParseFS(templates, "templates/*.html")
	if err != nil {
		panic(err)
	}
}

func renderHandler(w http.ResponseWriter, r *http.Request) {
	// Execute the template named "index.html".
	// The name matches the filename relative to the pattern.
	err := tmpl.ExecuteTemplate(w, "index.html", nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

The init function runs when the package loads. It parses the templates and stores them in tmpl. If parsing fails, the program panics. This is acceptable for startup code where recovery isn't possible. The handler executes the template by name. The name is the filename relative to the pattern, so templates/index.html becomes index.html.

Templates and assets belong in the binary when they don't change. Keep them external when users need to tweak them.

Patterns and gotchas

Embed patterns use filepath.Match rules. The pattern static/* matches all files and directories in static. If a directory matches, its contents are included recursively. The pattern *.html matches only HTML files in the current directory, not subdirectories.

You can combine multiple patterns in one directive. The compiler merges the results into a single filesystem.

//go:embed config.json templates/*.html static/*
// Merges config, templates, and static assets into one FS.
var allAssets embed.FS

The variable name doesn't affect the paths. The paths are determined by the pattern. If you embed static/*, you read static/index.html. If you embed *.html, you read index.html.

Patterns define the boundary. If the pattern doesn't match, the file doesn't exist.

Pitfalls and errors

The compiler enforces strict rules. If the directive is not immediately above the variable, the compiler ignores it. You get //go:embed must be in a comment directly preceding a variable declaration. Check for blank lines or extra comments.

If you try to read a file that wasn't matched by the pattern, ReadFile returns an error at runtime. The compiler doesn't check every possible read path. It only verifies that the pattern matches at least one file. If you embed templates/*.html and try to read templates/missing.html, the build succeeds, but the read fails.

The embedded filesystem is read-only. You cannot write to it. Attempts to modify files result in errors.

Binary size increases with embedded files. If you embed large assets, the binary grows. This matters for memory-constrained environments or slow network deployments. Measure the binary size before and after embedding.

The worst goroutine bug is the one that never logs. Similarly, the worst embed bug is the one that fails silently. Always check errors from ReadFile and template execution.

When to embed

Use embed when you need a single binary with no external dependencies. Use embed when you want to guarantee assets exist at build time. Use embed when deploying to environments with restricted filesystem access. Use embed when assets are part of the versioned codebase and change with the code.

Reach for external files when assets change frequently without recompiling. Reach for external files when the binary size limit is strict and assets are large. Reach for external files when users need to modify assets at runtime. Use http.Dir when you are serving files from the local disk during development and don't need embedding.

Embed makes deployment simple. External files make updates flexible. Pick based on your change frequency.

Where to go next