You can embed static files directly into your Go binaries, and //go:embed makes it surprisingly easy. The most counterintuitive thing about //go:embed is that it doesn’t actually embed the files into the binary executable itself in the way you might expect, but rather into the Go build cache, and then makes them available at runtime via the embed package.
Let’s see it in action. Imagine you have a simple web server that needs to serve an index.html file.
package main
import (
"embed"
"fmt"
"io/fs"
"net/http"
)
//go:embed static/*
var embeddedFiles embed.FS
func main() {
// Create a filesystem from the embedded files.
// The "static" directory is the root of our embedded filesystem.
fsys, err := fs.Sub(embeddedFiles, "static")
if err != nil {
panic(err)
}
// Serve the embedded files.
http.Handle("/", http.FileServer(http.FS(fsys)))
fmt.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
And you have a static directory with an index.html file:
<!-- static/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Embedded!</title>
</head>
<body>
<h1>Hello from embedded files!</h1>
</body>
</html>
When you run go build and then execute the binary, you’ll see "Server starting on :8080". Navigating to http://localhost:8080 in your browser will display "Hello from embedded files!". No index.html file needs to be present on the filesystem where the binary is run.
The //go:embed directive tells the Go compiler to include files specified by the path pattern into the compilation process. The embed.FS type from the embed package provides an io/fs.FS interface, allowing you to treat the embedded files as a virtual filesystem. The fs.Sub function is crucial here: it creates a sub-filesystem rooted at the "static" directory within your embedded files. This means that when you request /, http.FileServer looks for index.html inside the static directory of the embedded data, not the root of the embedded data.
The primary problem this solves is simplifying deployment. You no longer need to bundle separate asset files alongside your executable. Your entire application, including its static assets, is contained within a single binary. This is a massive win for distribution, containerization, and general ease of use. The embed package is designed to be as transparent as possible, acting like a regular os.DirFS or http.FileSystem but with no external dependencies.
You can embed individual files, directories, or use glob patterns. For instance, //go:embed config.json templates/*.html would embed a single config.json and all .html files within the templates directory. The embed.FS variable can be a map[string]string (for individual files), []byte (for a single file), or embed.FS (for directories and glob patterns). The embed.FS type is the most flexible, offering directory traversal and file reading capabilities.
The fs.Sub function is often misunderstood. If you //go:embed static/*, the embed.FS variable effectively contains a root directory with a "static" entry. If that "static" entry is itself a directory containing index.html, then fs.Sub(embeddedFiles, "static") correctly gives you a filesystem where index.html is at the root of that sub-filesystem. If you were to //go:embed static/index.html, embeddedFiles would directly contain index.html at its root, and fs.Sub(embeddedFiles, "static") would likely error because "static" wouldn’t be a directory within the embedded data.
When you use embed.FS, the underlying data is typically stored in the Go build cache. During compilation, Go reads the specified files and stores their content. At runtime, the embed package accesses this cached data. It’s not that the data is directly appended to the .text or .data sections of your ELF/PE executable in a readily readable format; rather, the Go toolchain manages this embedded data, making it accessible through the embed package’s API. This allows for efficient access and avoids inflating the binary size with duplicate data if multiple packages embed the same file.
The next concept you’ll want to explore is how to handle configuration files that might need to be overridden by external files when available, creating a hybrid approach to asset management.