How to Embed Files in a Go Binary with go

embed

Cli
Embed files into a Go binary using the //go:embed directive to include static assets at compile time.

The missing file problem

You build a command-line tool that generates reports. It needs a Markdown template, a default configuration file, and a few icon images. You ship the compiled binary to a teammate. They run it. The program crashes because it cannot find template.md in the current working directory. You forgot that os.Open looks for files relative to where the program runs, not where the binary lives. The fix is to stop treating those files as external dependencies and bake them directly into the executable.

How embedding actually works

The //go:embed directive tells the Go compiler to read specific files from your source tree and pack them into the binary during the build step. Think of it like printing a recipe and taping it to the inside cover of a cookbook. You do not need to keep the original index card on your desk anymore. The information travels with the book. At runtime, the embedded data lives in the program's memory space, accessible through standard Go types. No file system calls. No missing file panics. Just data that is guaranteed to be there.

A single file in memory

Start with a single text file. Create a file named data.txt in the same directory as your Go code. Put a short string inside it. Now write the Go code that claims it.

Here is the simplest way to bind a file to a variable:

package main

import (
    "embed"
    "fmt"
)

//go:embed data.txt
// The directive binds the file to the variable declared immediately below it
var rawContent []byte

func main() {
    // The slice already contains the file bytes. No os.Open call needed.
    // Converting to string prints the raw text to standard output.
    fmt.Println(string(rawContent))
}

The //go:embed comment must appear directly above the variable declaration. The compiler attaches the file contents to that variable. You can use []byte for raw data, string for text, or embed.FS for directory trees. The type you choose determines how you access the data later.

Goroutines are cheap. Embedded files are immutable. Treat them like constants.

What the compiler does behind the scenes

When you run go build, the compiler scans your source files for lines starting with //go:embed. It parses the file paths that follow. It reads those files from disk, verifies they exist, and converts their contents into Go constants or variable initializers. The resulting binary contains the file data in its read-only memory segment. When the program starts, the runtime loads that segment into memory. Your variable points directly to those bytes. There is zero file I/O at runtime.

The trade-off is a slightly larger binary. The benefit is deterministic behavior. You never ship a program that depends on an external file being in the right place. If you reference a file that does not exist, the build fails immediately. You will see an error like pattern data.txt: no matching files found. This is a feature. It catches missing assets before deployment.

Path resolution is relative to the directory containing the Go source file, not the project root. If your Go file lives in cmd/server/main.go but your assets are in assets/, the directive must use ../assets/*. Wildcards match files and directories recursively, but they do not match hidden files or files starting with a dot. If you need to exclude specific files, you cannot use a negation pattern. You must list the files explicitly or filter them in code.

Convention aside: Go's build system treats embedded files as part of the source set. Change an embedded file and you must rebuild. The compiler does not track file modification times for embedded assets the way it tracks Go source files. This keeps the build pipeline simple and reproducible. Trust the rebuild. Argue logic, not caching.

Serving a directory tree

Single files are useful, but production code usually needs directories. An HTTP server serving a frontend, a CLI tool with multiple configuration templates, or a database migration runner all benefit from embedding entire folder trees. The embed package provides embed.FS, a type that implements the standard fs.FS interface. It acts like a virtual file system inside your binary.

Here is how to embed a folder and serve it over HTTP:

package main

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

//go:embed static/*
// The wildcard pattern tells the compiler to include every file in the directory
var staticFiles embed.FS

func main() {
    // fs.Sub removes the leading "static/" path from embedded file names
    // This prevents the server from requiring /static/index.html in the browser
    subDir, err := fs.Sub(staticFiles, "static")
    if err != nil {
        panic(err)
    }

    // http.FS adapts the embedded FS to the http.FileServer interface
    // The handler now reads from memory instead of the disk
    handler := http.FileServer(http.FS(subDir))
    // Start the server. All requests are fulfilled from the binary itself.
    http.ListenAndServe(":8080", handler)
}

The fs.Sub call is the most important step here. Without it, the embedded file system retains the full path prefix. A request to /index.html would fail because the virtual file system expects /static/index.html. fs.Sub creates a new file system view that strips the prefix. The standard library handles the path normalization safely.

Convention aside: Go's standard library prefers composition over inheritance. embed.FS does not implement every method of a traditional file system. It implements the fs.FS interface, which is a minimal contract for reading files. This design keeps the interface small and allows third-party libraries to accept embedded assets without knowing they are embedded. The mantra here is "accept interfaces, return structs." You pass embed.FS to functions expecting fs.FS, and you never expose the concrete type unnecessarily.

Path handling and runtime behavior

Another common trap involves path separators. The embed.FS type always uses forward slashes internally, regardless of your operating system. If you construct paths using filepath.Join, Windows will inject backslashes and the lookup will fail. Stick to path.Join or plain string concatenation with forward slashes when working with embedded file systems. The fs package documentation explicitly states this invariant. Follow it and your code will run identically on Linux, macOS, and Windows.

Reading a file from embed.FS looks like standard file I/O, but the underlying mechanism is a memory slice lookup. You call fs.ReadFile or fs.Open on the embedded file system. The runtime searches the in-memory directory tree, finds the matching path, and returns a reader pointing to the preloaded bytes. No system calls. No disk latency. If you request a file that was not embedded, you get a standard fs.ErrNotExist error. Handle it the same way you handle any missing file.

Goroutines are cheap. Embedded files are immutable. Treat them like constants.

Testing and build-time safety

Testing code that relies on embedded files requires a slight shift in mindset. You cannot mock the file system easily because the data is baked into the binary. Instead, test the behavior that consumes the embedded data. If your program parses a JSON configuration file, write tests that pass the embedded JSON string to your parser function. If your program serves static assets, write tests that hit the HTTP handler and verify the response headers and status codes.

You can also use build constraints to swap embedded files during development. Create a config.dev.json and a config.prod.json. Use //go:build dev on one file and //go:build !dev on the other. Run go build -tags dev for local testing and go build for production. This pattern keeps your development workflow fast without sacrificing build-time safety.

The worst goroutine bug is the one that never logs. The worst embedded file bug is the one that fails silently at runtime. Let the compiler catch it.

When to embed and when not to

Use a []byte variable when you need a single configuration file, template, or certificate that you will read once or parse entirely into memory. Use an embed.FS variable when you have a directory tree of assets, static files, or migration scripts that need to be served or iterated over. Use external file paths when the data changes frequently at runtime, exceeds a few megabytes, or needs to be edited by the user without recompiling. Use a database or cloud storage when multiple services need to share the same large assets or when the data requires versioning and access control. Use a build-time code generator when you need to convert embedded data into Go structs, SQL queries, or protobuf definitions before the program runs.

Where to go next