Skip to content
C Codeloom
Go

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.

·4 min read · By Codeloom
Beginner 7 min read

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)
}
What embed bakes into the binary

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.