34.5 Embedding Files with go:embed
Right, so you’ve built your slick Go application. It works on your machine (famous last words), and now you need to ship it. You could tell your users to also download a config/ directory, a templates/ folder, and maybe some funny little .json files and hope they put everything in the exact right spot. Or, you could stop living in the 1990s and use go:embed.
This is one of those features that feels like cheating. Introduced in Go 1.16, go:embed gives you a compile-time mechanism to snatch up files from your filesystem and stuff them directly into your binary. The result? A truly self-contained application. No more worrying about the relative paths between your binary and its assets once it’s deployed. It’s all just… in there.
The magic is performed by the //go:embed directive. It’s a comment, but only in the same way a lightsaber is a “tool.” The compiler sees it and goes to work.
The Absolute Basics: Embedding a File
Let’s start by embedding a single text file. Imagine you have a file called greeting.txt with the text “Hello, World!” in it.
Your directory structure:
myapp/
├── main.go
└── greeting.txt
Here’s how you swallow that file into your binary:
package main
import (
_ "embed" // This import is crucial. The blank identifier is okay, we're side-effecting.
"fmt"
)
//go:embed greeting.txt
var greeting string
func main() {
fmt.Println(greeting) // Prints: Hello, World!
}
Yes, it’s that simple. The directive sits right above a variable declaration, and the compiler handles the rest. The variable type matters here; we used a string, but []byte works just as well if you want the raw bytes.
Embedding Multiple Files and Directories
Strings are cute, but you’ll probably want to embed a whole bunch of stuff: HTML templates, CSS, images, you name it. For that, we use the embed.FS type, which implements io/fs.FS. Think of it as a little virtual filesystem inside your binary.
Let’s say your project has a web/static directory:
myapp/
├── main.go
└── web/
└── static/
├── style.css
├── script.js
└── logo.png
Here’s how you embed the entire web/static directory:
package main
import (
"embed"
"io/fs"
"log"
"net/http"
)
//go:embed web/static
var staticFiles embed.FS
func main() {
// We need to serve the files, but 'staticFiles' contains a 'web/static' directory.
// We use fs.Sub to get an FS that starts from the 'web/static' subdirectory.
webRoot, err := fs.Sub(staticFiles, "web/static")
if err != nil {
log.Fatal(err)
}
// Now 'webRoot' behaves like a filesystem rooted at 'web/static'
fileServer := http.FileServer(http.FS(webRoot))
http.Handle("/static/", http.StripPrefix("/static", fileServer))
log.Println("Server started...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
This is the most common “gotcha.” Your embedded FS preserves the directory structure from the point of the directive. If you embed web/static, the files are accessible at web/static/style.css, not just style.css. Using fs.Sub is the cleanest way to shift the root of the filesystem to the directory you actually care about.
What Can You Actually Embed?
The rules are straightforward but strict, because this happens at compile time. The path in the directive must be relative to the source file containing it. The compiler will not follow symbolic links, and it will absolutely refuse to embed files outside your module root. This is a security feature, not a limitation. It prevents you from accidentally embedding your entire $HOME directory because you used an absolute path by mistake.
You can use patterns like *.txt or dir/*.png, but be warned: if your pattern matches nothing, the compiler will throw an error. Your build will fail. This is a good thing! A failing build is way better than a runtime surprise where half your assets are missing.
The Devil’s in the Details: Pitfalls and Best Practices
First, watch your file sizes. go:embed is fantastic, but it’s not meant for embedding a 4GB Blu-ray rip. You’re compiling these files into your binary, which means longer build times and massive binaries. It’s perfect for templates, static assets, and certificates. It’s not a content delivery network.
Second, remember it’s read-only. The embedded embed.FS is immutable. This is by design. If you need to modify a file at runtime, you must read it from the embedded FS and write it to a real filesystem (or in-memory store) first.
Third, be cautious with development vs production. When you’re actively editing your embedded CSS files, you need to restart your Go application for the changes to take effect, because the binary itself has to be rebuilt. This can break the flow of some live-reload development setups. Often, the best approach is to use a build tag during development to read from the live filesystem, and only use the embedded version for the final build. It’s a bit more setup, but your sanity is worth it.
Finally, the //go:embed directive is famously picky about formatting. There must be no space between the // and the go:embed. The variable declaration must immediately follow it, with no blank lines. If you get a //go:embed: invalid directive error, check your formatting. It’s almost always one of those two things.
So, use it. It turns the messy task of asset management into a solved problem. You get simplicity, portability, and reliability, all wrapped up in one binary. It’s a rare, unequivocal win.