Go embed Package Tutorial
Bundle static files, templates, and SQL migrations into your Go binary with the embed package. Learn the directive syntax, embed.FS usage, and where it shines versus a separate assets dir.
What you'll learn
- ✓What //go:embed does at compile time
- ✓How to embed strings, bytes, and filesystems
- ✓Serving embedded files over HTTP
- ✓Using embed.FS with templates and migrations
- ✓Limitations and gotchas
Prerequisites
- •Basic Go program structure
Before Go 1.16, shipping a binary that needed templates, SQL files, or static assets meant either a build script that turned files into Go source, or a deploy that copied a folder alongside the binary. The embed package made both unnecessary. Now you point a comment at a file and it lands inside the binary.
What embed is and why
embed is a standard library package that lets the compiler read files from disk and bake them into your binary as variables. The result is a single executable that can serve its own assets, render its own templates, and apply its own migrations without depending on the filesystem of the deployed host.
The reasons to use it are deployment simplicity and version safety. The assets ship with the code that needs them, at the exact revision that was tested. No drift, no missing files, no “works on staging” surprises.
Mental model
//go:embed is a compiler directive, not a runtime function. It is a magic comment that the Go compiler reads when the next variable is declared. The variable must be string, []byte, or embed.FS. The directive is resolved at build time against the package directory and frozen into the binary.
embed.FS implements fs.FS, which means any code that expects a filesystem (http.FS, template.ParseFS, sql/migrate) can use embedded assets transparently.
Hands-on example
Assume this directory layout.
main.go
templates/index.html
static/style.css
migrations/0001_init.sql
package main
import (
"embed"
"html/template"
"io/fs"
"net/http"
)
//go:embed templates/*.html
var tmplFS embed.FS
//go:embed static
var staticFS embed.FS
//go:embed migrations/*.sql
var migrations embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
func main() {
sub, _ := fs.Sub(staticFS, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", nil)
})
http.ListenAndServe(":8080", nil)
}
Common pitfalls
The directive must be on the line immediately before the variable. A blank line in between silently disables it, and your variable will be empty at runtime with no error.
Paths in //go:embed are relative to the source file, not the build root, and cannot escape the package directory. You cannot embed ../shared/assets because that path leaves the package. Move the files into the package or use a Go module symlink workaround.
Hidden files and files starting with _ are skipped by default. Use the all: prefix to include them, like //go:embed all:templates. This trips up people who name partials with leading underscores.
Embedded filesystems are read-only and use forward slashes regardless of OS. Do not concatenate with filepath.Join or you’ll produce backslashes on Windows and break lookups.
Practical tips
For HTTP file servers, always use fs.Sub to strip the directory prefix from URLs. Otherwise requests for /static/style.css look for static/static/style.css inside the embedded FS.
In tests, swap embed.FS for os.DirFS("./templates") via an fs.FS interface. That lets you edit templates without rebuilding while running with embedded assets in production.
Keep an eye on binary size. Embedding a few hundred kilobytes is free. Embedding a 200 MB video is technically allowed but inflates your image and slows builds. For large assets, a CDN is still the right answer.
If you need to embed a single file as a string for things like a SQL schema, prefer string over embed.FS so you can pass it directly to db.Exec without an extra read step.
Wrap-up
embed removes a category of deployment bugs by making your binary self-contained. Place the directive on the line above the variable, use embed.FS for trees, and reach for fs.Sub when serving HTTP. With those habits, your service ships as one file that has everything it needs.
Related articles
- Go Go database/sql Tutorial
Use Go's standard database/sql package the right way: drivers, connection pools, prepared statements, transactions, context cancellation, and avoiding the classic Rows.Close leak.
- Go Go Build Tags Explained
Use Go build tags to include or exclude files per OS, architecture, or custom condition. Learn the new //go:build syntax, common patterns, and how tags interact with the test runner.
- Go Go Context Cancellation Patterns
Master Go's context package: propagate deadlines, cancel goroutines safely, and avoid leaks with practical patterns for HTTP, database, and pipeline code.
- Go Go context Package Explained
How to use Go's context package effectively: cancellation, deadlines, propagation, request-scoped values, and the patterns that keep services responsive.