How to Use go

embed to Embed Templates in Your Binary

Web
Embed template files into your Go binary using the //go:embed directive and embed.FS type.

The missing file problem

You build a Go binary. You copy it to a server. You run it. The app starts, but the first request crashes because index.html isn't there. You forgot to deploy the templates directory. This is the classic deployment gap. Go solves it with go:embed. The directive bakes files into the binary at compile time. The result is a single executable that contains everything it needs. No external assets. No missing files. No deployment scripts to copy static content.

How embedding works

The //go:embed directive tells the Go compiler to read files from disk and pack them into the executable. The compiler treats the files as part of the source code. When you run go build, the resulting binary contains the file bytes. At runtime, the program reads the data from memory, not from the filesystem.

Think of it like a Swiss Army knife. The tools are built into the handle. You don't carry a separate toolbox. The knife is one object, and it has everything you need. go:embed turns your binary into that knife. The templates, images, and configuration files are inside the metal.

The directive requires the embed package. The comment must sit directly above a package-level variable. The variable type determines how the data is stored. You can embed a single file as a string or []byte. You can embed multiple files into an embed.FS variable, which creates a virtual filesystem. The embed.FS type implements the fs.FS interface, so it works with standard library tools like html/template and net/http.

Minimal example

Here's the simplest case: embed a single text file as a string. The variable holds the file content directly.

package main

import (
	"embed" // required for the go:embed directive to work
	"fmt"
)

//go:embed hello.txt
// embeds the file into the variable as a string
var content string

func main() {
	fmt.Println(content) // prints the file content directly
}

The compiler reads hello.txt and puts its bytes into content. If hello.txt doesn't exist, the build fails. The compiler rejects the program with pattern matches no files. This error saves you from shipping a binary with missing assets. The build failure forces you to fix the problem before deployment.

Compile-time behavior

The compiler scans your source files for //go:embed comments. It resolves the path relative to the source file location. It checks if the files exist. If a file is missing, the build stops. The compiler reads the file contents and generates code to embed them. For embed.FS, the compiler creates a virtual filesystem structure inside the binary.

The path resolution is relative to the .go file, not the module root. If your file is cmd/server/main.go, and you write //go:embed templates/*.html, the compiler looks for cmd/server/templates/*.html. It does not look relative to the project root. Move the .go file, and the embed breaks. Keep assets close to the code that uses them.

The embed package import is mandatory. If you forget it, the compiler rejects the directive with go:embed: missing import of package embed. The directive is a comment, but the compiler parses it only when the package is present. This prevents accidental embedding in code that doesn't intend to use it.

Realistic example

Real apps usually need multiple files. Use embed.FS to create a virtual filesystem. Here's an HTTP server that serves HTML templates from embedded files. The embed.FS variable holds all the templates. The template.ParseFS function reads them from the embedded filesystem.

package main

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

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

func main() {
	// parse templates from the embedded filesystem
	t, err := template.ParseFS(templateFS, "templates/*.html")
	if err != nil {
		panic(err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// execute the template against the response writer
		t.ExecuteTemplate(w, "index.html", nil)
	})

	http.ListenAndServe(":8080", nil)
}

The templateFS variable implements fs.FS. The template package uses this interface to read files. When ExecuteTemplate runs, it calls Open on the filesystem to get the template content. The embed.FS returns the embedded bytes. This design means any library that accepts fs.FS works with embedded assets. You can use http.FS to serve static files. You can use filepath.WalkDir to iterate over embedded directories. The standard library is built for this abstraction.

Pitfalls and errors

Embedding has rules. Break them, and the compiler stops you.

The directive must be on a package-level variable. If you put it on a function or a local variable, the compiler rejects it with go:embed: must be package level. Package-level variables are the only place the compiler can store embedded data.

Wildcards match files, not directories. //go:embed assets/* embeds files in assets, but not files in assets/subdir. Use //go:embed assets/** for recursive embedding. The double asterisk matches all files in subdirectories. This is supported in Go 1.16 and later.

You cannot embed directories or symlinks. The compiler rejects these with go:embed: cannot embed non-regular file. Embed only regular files. If you need a directory structure, embed the files inside it. The embed.FS preserves the directory hierarchy in memory.

embed.FS is read-only. You cannot write to it. It is for assets only. If you need writable storage, use the real filesystem or a database. Trying to write to an embedded file will fail at runtime.

Embedding large files increases binary size. A 10MB video file makes a 10MB larger binary. This slows down deployment and increases memory usage. Embed only what is necessary. Templates, CSS, small images, and configuration defaults are good candidates. Large assets belong on a CDN or in a database.

The compiler error pattern matches no files is common. It happens if the directory is empty, the glob is wrong, or the path is incorrect. Check the path relative to the source file. Check the wildcard syntax. Ensure the files exist before building.

When to embed

Use go:embed when you need a single binary that runs anywhere without asset setup. Use go:embed when assets are static and small, like HTML templates, CSS, images, or default configuration files. Use go:embed when you want to guarantee that the binary and assets are always in sync. Use external files when assets change frequently and you want to update them without rebuilding. Use a database when assets are dynamic or user-generated. Use a CDN when assets are large and need to be cached globally.

The compiler is your asset manager. If the build passes, the files are there. Embed small things. Stream big things. Relative paths follow the source file, not the module root.

Where to go next