The missing file bug
You build your Go web server. It runs perfectly on your laptop. You copy the binary to the production server. You run it. The server starts, but every request returns a 404. The template files never made it to the server. You forgot to upload the templates directory, or the path is wrong, or the deployment script skipped static assets. This is a common deployment headache. Go has a solution that eliminates this class of bugs entirely: embed the files directly into the binary.
The embed package lets you pack files into your executable at compile time. The compiler reads the files from your disk and turns them into a read-only file system inside the binary. At runtime, your program accesses these files through an embed.FS variable. No external dependencies. No missing files. One binary does everything.
Think of it like baking cookies into a cake. You don't serve the cake and hope the cookies are in a separate jar. The cookies are inside the cake. When you move the cake, the cookies move with it.
Minimal example
Here's the simplest setup: define the embedded file system, parse the templates, and render one.
package main
import (
"embed"
"html/template"
"log"
"os"
)
//go:embed templates/*.html
var templatesFS embed.FS
func main() {
// ParseFS reads the embedded FS and compiles all matching templates.
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
if err != nil {
log.Fatal(err)
}
// ExecuteTemplate renders the named template to stdout.
if err := tmpl.ExecuteTemplate(os.Stdout, "index.html", nil); err != nil {
log.Fatal(err)
}
}
The //go:embed directive tells the compiler to include files matching the pattern. The directive must sit immediately above the variable declaration. The variable type must be embed.FS or []byte. The compiler rejects the program with //go:embed must be immediately followed by a variable declaration if you insert a comment or blank line between the directive and the variable.
Embed once, run anywhere.
How the compiler and runtime handle embedded files
When you run go build, the compiler scans the source files for //go:embed directives. It resolves the file paths relative to the directory containing the source file. It reads the content of every matching file and injects the bytes into the binary. The compiler also generates the templatesFS variable, which implements the io/fs.FS interface.
At runtime, templatesFS behaves like a file system, but it lives in memory. It is read-only. You cannot write to it. The template.ParseFS function takes the embed.FS and a pattern. It uses the fs.Glob function internally to find all files matching the pattern. For each file, it reads the content and parses the template syntax.
ParseFS returns a single *template.Template that contains all the parsed templates linked together. This is a powerful feature. If you have base.html and index.html, and index.html uses {{template "base.html"}}, the parser resolves the reference automatically. You don't need to parse files individually. The template set handles composition.
The if err != nil check is verbose by design. The Go community accepts the boilerplate because it makes the unhappy path visible. If parsing fails, the error tells you exactly which file or line caused the problem.
Realistic HTTP handler
Here's a realistic setup for a web server. The code parses templates at startup and serves them in an HTTP handler.
//go:embed templates/*.html
var templatesFS embed.FS
// tmpl holds the parsed templates, initialized at startup.
var tmpl *template.Template
func init() {
// ParseFS compiles templates from the embedded file system.
// Must panics on error, failing fast if templates are broken.
tmpl = template.Must(template.ParseFS(templatesFS, "templates/*.html"))
}
The init function runs before main. template.Must is a helper that panics if the error is non-nil. This is safe for templates that must exist for the application to function. A broken template is a programming error, not a runtime condition. Panicking at startup stops the program immediately, which is better than serving broken pages.
Convention aside: template.Must follows the pattern of panicking for configuration errors at startup while returning errors for runtime failures. Use Must for things that cannot fail in a correct program.
func indexHandler(w http.ResponseWriter, r *http.Request) {
// Struct literal provides the data context for the template.
data := struct {
Title string
Items []string
}{
Title: "Embedded Templates",
Items: []string{"Fast", "Portable", "Safe"},
}
// ExecuteTemplate renders the named template to the response.
if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
The handler creates a data struct and passes it to ExecuteTemplate. The template engine interpolates the values into the HTML. ExecuteTemplate writes the result to the http.ResponseWriter. If rendering fails, the handler logs the error and returns a 500 status.
Here's an example template file that the embedded file system would contain.
<!-- templates/index.html -->
<!-- The template defines the structure and uses Go template syntax. -->
<!DOCTYPE html>
<html>
<head>
<!-- Title is interpolated from the data struct. -->
<title>{{.Title}}</title>
</head>
<body>
<!-- Range iterates over the Items slice. -->
<ul>
{{range .Items}}
<li>{{.}}</li>
{{end}}
</ul>
</body>
</html>
The html/template package auto-escapes values to prevent cross-site scripting attacks. If you pass a string containing <script>, the template engine converts it to <script>. Use html/template for HTML output. Use text/template only for non-HTML formats like plain text or JSON, where escaping would break the format.
Panic early on bad templates. Serve safely at runtime.
Pitfalls and compiler rules
The //go:embed directive has strict syntax rules. The pattern must be a valid file glob. The compiler resolves paths relative to the source file. If you move the Go file, the paths might break. Keep the source file close to the assets it embeds.
The compiler rejects the program with //go:embed pattern does not match any files if the pattern matches nothing. This helps catch typos early. However, if you delete a file after building, the binary still contains the old version. The binary size does not shrink automatically. You must rebuild to remove files.
Embedding large files increases the binary size. Don't embed 100MB of images or video. Embed small files like templates, icons, or configuration snippets. If your binary grows too large, consider serving large assets from a CDN or external storage.
The embed.FS is read-only. You cannot modify files at runtime. If you need to update templates without rebuilding, use os.ReadFile or a hot-reload mechanism instead.
Goroutine leaks happen when a goroutine waits on a channel that never gets closed. This doesn't apply to embed, but it's worth remembering when building concurrent servers that serve embedded assets. Always have a cancellation path for long-running tasks.
The compiler guards the directive syntax. Trust the build process.
When to embed and when not to
Use embed when your application needs static assets like templates, images, or configuration files that must travel with the binary. Use embed when you want a single-file deployment without worrying about directory structures on the target machine. Use embed when the files change rarely and are part of the application code. Use os.ReadFile when the files change frequently at runtime and you need to reload them without rebuilding. Use external configuration files when operators need to tweak values without redeploying the binary. Use plain string literals for tiny templates that change rarely and fit comfortably in the source code.
One binary, zero dependencies.