How to Use go

embed to Embed Files in Go Binaries

Use the //go:embed directive to compile files directly into your Go binary for self-contained distribution.

The missing file problem

You built a CLI tool that generates reports. It works perfectly on your machine. You zip the binary, send it to a colleague, and they run it. The program crashes immediately. The error points to a missing template.html file in the current directory. You forgot to tell them to copy the assets alongside the binary. Or worse, you ship a Docker image, the volume mount gets misconfigured, and the app starts with empty templates.

This happens because Go programs don't automatically know about files on your disk. The compiler packages your code, but it ignores data.txt, static/, and templates/ unless you tell it otherwise. The standard workflow involves copying assets to the output directory, which adds steps to your build script and creates deployment friction. If the copy step fails, the binary is broken at runtime.

Go provides a way to bake files directly into the executable. The embed package lets you compile files into the binary so they travel with the code. No external dependencies. No copy steps. The file data lives inside the executable, ready to use the moment the program starts.

How embedding works

The embed package works at compile time. When the compiler sees a //go:embed directive, it reads the specified file or directory and injects the contents into the binary. The resulting executable contains the file bytes in its data segment. At runtime, the variable is already initialized with the data. No file I/O happens. The program never touches the disk to load the embedded content.

Think of it like a Swiss Army knife. Instead of carrying a separate screwdriver in your pocket, the tool contains the screwdriver inside the handle. When you run the program, the data is already there in memory. The file path is resolved relative to the source file during the build, not the working directory at runtime. This means the build fails if the file is missing, which is safer than a runtime crash.

Embedding increases the binary size. The file data becomes part of the executable. If you embed a 10 megabyte file, the binary grows by 10 megabytes. This is fine for templates, configs, and small assets. It is not suitable for large datasets or media files. The trade-off is deployment simplicity versus binary size.

Minimal example

Here's the simplest way to embed a single text file. The directive goes immediately before the variable declaration. The variable type must be string, []byte, or embed.FS.

package main

import (
    "embed"
    "fmt"
)

//go:embed hello.txt
// content stores the file data as a string literal
var content string

func main() {
    // print the embedded content to stdout
    fmt.Println(content)
}

The //go:embed hello.txt line is a special comment. The compiler parses it and looks for hello.txt in the same directory as the source file. If the file exists, the compiler reads its contents and replaces the variable with the data. The content variable holds the file bytes as a string. If hello.txt contains Hello, World!\n, the variable holds exactly that text.

The build fails if the file is missing. The compiler rejects the program with pattern hello.txt: no matching files found. This error happens during go build, not at runtime. You catch the problem early. The directive supports glob patterns. You can use *.txt to match multiple files, but the variable type must be embed.FS for multiple files. A string variable can only hold one file.

Embedding a single file into a string is useful for version strings, license text, or small configuration snippets. The data is accessible as a normal string. You can pass it to functions, split it, or print it. The compiler handles the injection. You write normal Go code to use the data.

Directories and embed.FS

Most applications need more than one file. You might have a directory of static assets, multiple templates, or a bundle of configuration files. The embed.FS type handles directories. It creates a virtual file system inside the binary. The embed.FS type implements the fs.FS interface, which means it works with standard library functions that accept file systems.

Here's how to embed a directory and serve it over HTTP. The http.FS wrapper strips the prefix from paths, which is necessary because embedded paths include the directory name.

package main

import (
    "embed"
    "net/http"
    "os"
)

//go:embed static/*
// assets holds a virtual filesystem of the static directory
var assets embed.FS

func main() {
    // serve the embedded files over HTTP
    http.Handle("/", http.FileServer(http.FS(assets)))
    // listen on port 8080
    if err := http.ListenAndServe(":8080", nil); err != nil {
        // handle server startup error
        fmt.Fprintf(os.Stderr, "server failed: %v\n", err)
        os.Exit(1)
    }
}

The //go:embed static/* directive embeds everything inside the static directory. The assets variable holds the virtual file system. The http.FS(assets) call wraps the embedded file system and strips the static/ prefix. Without this wrapper, requests would need to include the prefix, like /static/index.html. The wrapper makes the paths clean, like /index.html.

The http.FileServer handler serves files from the file system. It handles directory listings, content types, and range requests. The embedded file system behaves like a real directory, but the data comes from memory. The server starts and serves the files. No disk access occurs. The if err != nil check follows Go convention for error handling. The boilerplate makes the unhappy path visible. If the server fails to start, the program logs the error and exits.

You can read files from embed.FS manually using fs.ReadFile or fs.ReadDir. The embed.FS type has an Open method that returns a file handle. You can iterate over directories, check file sizes, and read contents. The API matches the standard os package, so you can switch between embedded and disk-based files by changing the file system variable.

Realistic example: Templates

Web applications often use HTML templates. The text/template package can parse templates from a file system. The template.ParseFS function reads templates from an fs.FS implementation. This is the idiomatic way to load templates from embedded assets.

package main

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

//go:embed templates/*.html
// tpl holds the embedded template files
var tpl embed.FS

// globalTemplate stores the parsed templates
var globalTemplate *template.Template

func init() {
    // parse all html files in the embedded filesystem
    parsed := template.Must(template.ParseFS(tpl, "templates/*.html"))
    // store for use in handlers
    globalTemplate = parsed
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // execute the index template
        if err := globalTemplate.ExecuteTemplate(w, "index.html", nil); err != nil {
            http.Error(w, "template error", http.StatusInternalServerError)
        }
    })
    http.ListenAndServe(":8080", nil)
}

The //go:embed templates/*.html directive embeds all HTML files in the templates directory. The init function runs when the package loads. It calls template.ParseFS to parse the templates from the embedded file system. The template.Must function panics if parsing fails. This is idiomatic for initialization code. Template errors should crash the program at startup, not fail silently at runtime. The parsed templates are stored in a global variable for use in handlers.

The handler executes the index.html template. The ExecuteTemplate method renders the template and writes the output to the response writer. If rendering fails, the handler returns a 500 error. The template data is already in memory. No disk I/O happens during the request. The response is fast.

The templates/*.html pattern in ParseFS matches the embedded files. The pattern is relative to the file system root. Since the directive embeds templates/*, the paths inside the file system start with templates/. The pattern matches correctly. If you change the directive to //go:embed templates, the paths would be different, and the pattern would need adjustment.

Pitfalls and compiler errors

The compiler catches most mistakes early. If you reference a file that doesn't exist, the build fails with pattern static/*: no matching files found. This prevents runtime surprises. The path is relative to the source file. If you move the source file, the build breaks. This is intentional. It keeps the build reproducible.

Symlinks cause issues. The go:embed directive does not follow symlinks by default. If you have a symlink in the directory, the compiler rejects it with go:embed: cannot embed non-regular file. You need to resolve symlinks before building or avoid them. Some build systems create symlinks for convenience. You must flatten the directory structure or use a build script to copy files.

Embedding large files increases binary size. If you embed a 500 megabyte database, the binary becomes huge. The build takes longer. The binary takes longer to download. The program consumes more memory. Don't embed large datasets. Use external storage for large files. Embedding is for assets that are small and static.

The directive must be immediately before the variable. You cannot put code between the directive and the variable. The compiler rejects this with go:embed: missing variable declaration. You can have comments between the directive and the variable, but the directive must be the last comment. The variable type must be valid. You cannot embed into an int or a struct. The compiler complains with invalid embed type. Stick to string, []byte, or embed.FS.

The embed package is part of the standard library. You don't need third-party tools. The directive is supported by the Go compiler. It works with go build, go run, and go test. The behavior is consistent across environments. The gofmt tool preserves the directive because it's a comment. You don't need to worry about formatting tools removing it. Just keep the directive adjacent to the variable.

Embed files, not megabytes. Keep the binary lean.

When to use embedding

Use //go:embed with a string variable when you need a single small text file like a version string or a simple template. Use //go:embed with a []byte variable when you need raw binary data like a certificate or a small image. Use //go:embed with embed.FS when you have a directory of assets like static files or multiple templates. Use external file loading when the data changes frequently and you don't want to rebuild the binary. Use a database or remote API when the data is large or shared across many instances.

Bake it in if it never changes. Load it if it does.

Where to go next