How to Embed Configuration Files in Go

Use the `embed` package (available in Go 1.16+) to compile configuration files directly into your binary, eliminating the need for external file dependencies at runtime.

The missing config file problem

You build your Go binary, copy it to a production server, and run it. The application crashes immediately. The log shows a panic: it cannot find config.yaml. You spent ten minutes checking network permissions and user roles, only to realize the config file never made it to the server. This happens because Go binaries are self-contained executables. They do not automatically bundle the files sitting next to the source code.

The embed package solves this by baking files directly into your binary during compilation. The compiler reads the file contents and stores them inside the executable. When the program runs, it reads from memory, not the disk. This turns your application into a single file that contains everything it needs. No external dependencies. No missing files. The binary is portable and immutable.

How embedding works

The embed package uses a compiler directive to mark variables for injection. The directive looks like a comment, but the compiler treats it as an instruction. When you run go build, the compiler scans the source files for //go:embed directives. It reads the files specified in the directive and injects the bytes into the corresponding variable.

The result is a binary that carries the file data. At runtime, the variable holds the contents. The file system is never touched. If the file does not exist when you compile, the build fails. If the file changes after compilation, the binary does not change. You must rebuild to pick up updates. This behavior makes embed ideal for static assets, default configurations, and templates that should not change between deployments.

The embed package was introduced in Go 1.16. Before that, developers used go generate scripts to convert files into Go source code. That approach worked, but it added a build step and made the generated code hard to read. The embed package is built into the compiler. It is faster, safer, and requires no extra tools.

Embedding a single file

Here's the simplest case: embedding a single file as raw bytes. This pattern works well for small configuration files like JSON or YAML that you parse once at startup.

package main

import (
	"encoding/json"
	"embed"
	"fmt"
	"log"
)

//go:embed config.json
// The directive must sit immediately above the variable.
// It tells the compiler to read config.json and store its contents here.
var configData []byte

type Config struct {
	Port int    `json:"port"`
	Host string `json:"host"`
}

func main() {
	var cfg Config
	// Unmarshal the embedded bytes into the struct.
	if err := json.Unmarshal(configData, &cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Loaded config: %s:%d\n", cfg.Host, cfg.Port)
}

The //go:embed directive specifies the file path. The path is relative to the directory containing the Go source file. The variable type must be string, []byte, or embed.FS. In this example, the variable is []byte, which holds the raw file contents. The json.Unmarshal function parses the bytes into the Config struct. The code runs entirely in memory. No disk I/O occurs after the binary starts.

Convention aside: The embed directive uses a comment syntax. This keeps the code clean and allows gofmt to format the rest of the file normally. The community convention is to place the directive on the line immediately above the variable. Do not put blank lines between the directive and the variable. The compiler requires them to be adjacent.

Embedding directories with embed.FS

Real applications often have multiple config files, templates, or assets. Embedding each one as a separate variable gets messy. Use embed.FS to embed a directory tree. The embed.FS type implements the io/fs.FS interface, which gives you a virtual file system accessible via standard library functions.

package main

import (
	"embed"
	"fmt"
	"log"
)

//go:embed configs/*.yaml
// Embeds all yaml files in the configs directory.
// The variable type must be embed.FS.
var ConfigFS embed.FS

func loadConfig(env string) ([]byte, error) {
	// Path is relative to the directory containing the Go source file.
	path := fmt.Sprintf("configs/%s.yaml", env)
	// ReadFile reads from the embedded FS, not the disk.
	return ConfigFS.ReadFile(path)
}

func main() {
	data, err := loadConfig("production")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Loaded %d bytes from embedded FS\n", len(data))
}

The directive uses a glob pattern to match multiple files. The pattern configs/*.yaml embeds all YAML files in the configs directory. The ReadFile method works like os.ReadFile, but it reads from the embedded FS. The path argument is relative to the embed pattern. If you embed configs/*.yaml, the path must start with configs/.

The embed.FS type integrates with many standard library functions. You can pass it to http.FileServer to serve static assets. You can pass it to template.ParseFS to parse HTML templates. You can pass it to fs.WalkDir to iterate over the embedded files. This integration makes embed.FS a powerful tool for building self-contained applications.

Convention aside: When using embed.FS, name the variable descriptively. Use ConfigFS, Templates, or StaticAssets rather than just FS. This makes the code easier to read and maintain. The variable is usually package-level, but you can also return an embed.FS from a function if you need to encapsulate the embedding logic.

Serving embedded assets

One of the most common uses for embed.FS is serving static files over HTTP. You can embed HTML, CSS, JavaScript, and images, then serve them using the standard net/http package. This creates a single binary that contains both the application logic and the frontend assets.

package main

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

//go:embed static/*
// Embeds the static directory for serving assets.
var staticFS embed.FS

func main() {
	// http.FS wraps embed.FS to satisfy the fs.FS interface.
	fileServer := http.FileServer(http.FS(staticFS))
	// StripPrefix removes the /static/ prefix from the request path.
	// This maps /static/style.css to static/style.css in the FS.
	http.Handle("/static/", http.StripPrefix("/static/", fileServer))
	// ListenAndServe blocks until the process exits.
	http.ListenAndServe(":8080", nil)
}

The http.FS function wraps the embed.FS to satisfy the fs.FS interface required by http.FileServer. The StripPrefix handler removes the /static/ prefix from the request URL. This maps requests like /static/style.css to static/style.css in the embedded FS. Without StripPrefix, the server would look for static/static/style.css, which does not exist.

This pattern is widely used in Go web applications. It eliminates the need for a separate web server to serve static files. The binary handles everything. Deployment becomes a single file copy.

Pitfalls and compiler errors

The //go:embed directive has strict rules. Violating these rules causes compile-time errors. The compiler is your safety net. If the directive is wrong, the build fails. Trust the error messages.

The directive must appear immediately before the variable declaration. No blank lines are allowed. If you put a comment or an empty line between the directive and the variable, the compiler ignores the directive and the variable stays empty. The compiler rejects this with //go:embed must be followed by an identifier or ( ... ).

The path is relative to the directory containing the Go source file. If your Go file is in internal/config/config.go, the path config.yaml refers to internal/config/config.yaml. It does not refer to the root of the module. This trips up developers who move files around. Always check the relative path.

If the file does not exist at compile time, the build fails. The compiler reports pattern config.json: no matching files found. This error is helpful. It tells you exactly which file is missing. You cannot embed files that are not present during the build.

You cannot embed files outside the module directory. The compiler enforces this to keep builds reproducible. If you try to embed ../other/module/file.txt, the build fails with an error about the path being outside the module. This restriction ensures that the binary does not depend on the local directory structure outside the module.

Another pitfall is hot-swapping. Embedded files are immutable. If you change the file on disk, the binary does not change. You must rebuild to pick up updates. This makes embed unsuitable for configurations that must change at runtime without rebuilding. Use external file loading or environment variables for mutable configuration.

Never embed secrets. If you embed a password or API key, it becomes part of the binary. Anyone with access to the binary can extract the secret using a hex editor or string search. Use environment variables or a secrets manager for sensitive data. Embed only non-sensitive defaults and assets.

Convention aside: The embed package is part of the standard library. No third-party dependencies are needed. The embed.FS type implements io/fs.FS, which is the standard interface for file systems in Go. This means your embedded FS works with any function that accepts an fs.FS. This includes functions from third-party libraries that follow Go conventions.

When to use embed

Use //go:embed with a []byte variable when you have a single small file like a JSON config or a SQL migration that you parse once at startup.

Use embed.FS when you need to embed multiple files, directories, or serve static assets like HTML templates and CSS.

Use embed.FS with http.FileServer when you want to serve embedded files over HTTP without writing a custom handler.

Use embed.FS with template.ParseFS when you need to parse HTML templates from embedded files.

Use external file loading when the configuration must change at runtime without rebuilding the binary, such as feature flags or environment-specific settings.

Use environment variables for sensitive data like database passwords and API keys. Never embed secrets in the binary.

Embed for immutability. Load externally for mutability.

Where to go next