AlgoMaster Logo

embed Directive (//go:embed)

Medium Priority37 min readUpdated June 6, 2026

Shipping a Go program used to mean shipping the binary plus a folder of HTML templates, CSS, SQL migrations, and config defaults that the program loaded at runtime. Lose one of those files in the wrong directory and the service refused to start. Go 1.16 added the //go:embed directive so the compiler can bake those files directly into the binary, turning deployment back into "copy one executable and run it." This lesson covers how //go:embed works, the three target types it supports, the pattern rules and constraints, and the standard library APIs that consume the result.

Why //go:embed Exists

Before Go 1.16, embedding static assets in a binary required a third-party tool. The popular options were go-bindata, packr, and statik. They all worked the same way: read each asset file, generate a .go source file that contained the bytes as a string or byte slice literal, and check that generated file into the repository. The build then compiled the generated source alongside everything else.

The pre-1.16 workflow had real costs:

  • The generated files were huge. A folder of HTML templates produced a multi-megabyte .go file full of byte literals. Code review tooling, diffs, and IDEs all groaned under the weight.
  • You had to remember to regenerate after editing any asset. A stale generated file shipped silently and overrode the change.
  • Each tool had its own pattern syntax, its own API for reading the bundled files, and its own performance quirks. Switching tools meant rewriting the load paths everywhere.
  • New contributors had to install the right CLI before they could build the project.

//go:embed collapses all of that into a compiler-supported comment directive. There's no code generation step, no third-party install, and the embedded files appear in the binary at build time without an intermediate generated source file. The standard library handles it natively, so the API is the same in every Go program.

A Go 1.16+ project that previously used go-bindata typically replaces a hundred lines of build glue with a single line above a variable declaration:

That comment is the entire mechanism. The Go compiler reads it, finds the matching files, and embeds their contents directly into the program.

Syntax of //go:embed

The directive is a magic comment. It must appear immediately above a package-level variable declaration, with no blank line between the comment and the var. The comment text is //go:embed followed by one or more space-separated patterns.

Assume welcome.txt sits next to main.go and contains:

A few rules apply to where the directive can appear:

  • The directive must sit on the line directly above the var declaration. A blank line, a doc comment, or any other line between them breaks the association and the compiler will report go:embed cannot apply to var inside func or misplaced go:embed directive.
  • The target must be a package-level var. You cannot embed into a local variable inside a function.
  • The target variable's type must be string, []byte, or embed.FS. Anything else is a compile error.

Importing the embed Package

Even when the target variable is a plain string or []byte, the package must import "embed". The directive is only recognized in files that import the embed package, even if your code never references any symbol from it.

For string and []byte targets you don't use any name from the embed package, so the import has to be the blank form:

For embed.FS targets you use the embed.FS type directly, so the import is the normal form:

Forget the import and the compiler emits:

The reason for the rule is so that grep'ing for "embed" in a file's imports is enough to tell whether the file uses the directive. The compiler enforces the convention rather than guessing.

The Three Target Types

A variable annotated with //go:embed must be one of string, []byte, or embed.FS. The choice depends on whether you're embedding one file or many, and whether the contents are text or binary.

Target typeHoldsWhen to useExample pattern
stringContents of a single file as UTF-8 textBanners, license text, one HTML template, default config//go:embed welcome.txt
[]byteContents of a single file as raw bytesBinary blobs, images, single PDFs, when you need a mutable copy//go:embed assets/logo.png
embed.FSRead-only filesystem tree of one or more files and directoriesMultiple templates, static assets, SQL migrations, anything with structure//go:embed templates static

Embedding a Single File as a String

The text banner above used the string form. Here's a slightly larger example: a CLI store tool prints a welcome screen plus a footer pulled in from another single file.

Assume the project layout is:

With welcome.txt containing:

And footer.txt containing:

Each //go:embed directive applies only to the variable directly underneath it. You can have many such directives in one file, each pointing at a different file and a different variable.

Embedding a Single File as Bytes

Use []byte when the file isn't text or when you want a mutable copy. A common case for the e-commerce theme is a default product-category CSV that the loader parses on startup.

categories.csv:

Pick []byte over string when the contents are not guaranteed UTF-8, when you plan to pass them to a parser that wants a byte slice, or when you want a copy you can mutate without affecting the embedded data. The slice you get from a []byte target is a fresh copy each time the variable is read; the embedded bytes themselves are not modifiable.

Embedding Multiple Files with embed.FS

embed.FS is the interesting target. It exposes a whole tree of files as a read-only filesystem rooted at the package directory. One pattern can match many files, and the result behaves like the fs.FS interface.

embed.FS is a concrete type defined in the standard library. It has three methods: Open(name string) (fs.File, error), ReadDir(name string) ([]fs.DirEntry, error), and ReadFile(name string) ([]byte, error). That's enough to satisfy fs.FS, fs.ReadDirFS, and fs.ReadFileFS, which is why functions like template.ParseFS and http.FileServer accept it directly.

Pattern Rules

The text after //go:embed is a space-separated list of patterns. Each pattern selects one or more files. The grammar is small, but the details matter.

Literal Filenames

The simplest pattern is a single literal filename, relative to the file containing the directive. //go:embed welcome.txt matches the file named welcome.txt in the same directory as the source file. //go:embed banners/welcome.txt matches that file under the banners subdirectory.

The filename is interpreted as a forward-slash path on every platform, including Windows. The directive is portable: write templates/order.html, not templates\order.html.

Glob Patterns

Patterns support the path.Match glob syntax. The wildcards are:

  • * matches any sequence of non-separator characters.
  • ? matches any single non-separator character.
  • [abc] matches any one of the listed characters.

//go:embed templates/*.html matches every .html file directly inside the templates directory but does not descend into subdirectories. //go:embed migrations/*.sql matches every .sql migration file in the migrations directory.

Directory Names Recurse

Naming a directory in the pattern embeds every regular file under that directory and all of its subdirectories. //go:embed templates matches templates/order.html, templates/email/welcome.html, and templates/email/admin/notice.html in one shot.

This is the simplest way to embed a whole asset tree. The glob templates/*.html only matches files in the top level; templates alone (no glob) recurses.

The all: Prefix

By default //go:embed skips files and directories whose names begin with . or _. This matches the Go toolchain's general convention of treating dotfiles and underscore-prefixed names as hidden or generated.

If you want those files included, prefix the pattern with all:. So //go:embed all:templates includes templates/_draft.html and templates/.config.json, while plain //go:embed templates skips both.

The all: prefix applies to one pattern. Combine it with multiple patterns by writing all: only where you need it: //go:embed templates all:secrets includes hidden files under secrets but not under templates.

Multiple Patterns

A single //go:embed line can list multiple patterns separated by spaces. All matches are merged into the same target. For an embed.FS target, the result is one filesystem containing every matched file.

That directive embeds both the templates directory and the static directory into one embed.FS. Inside the FS, files keep their relative paths: templates/index.html, static/style.css, and so on.

A single variable can also have multiple //go:embed lines stacked above it, which is sometimes clearer:

The two forms are equivalent.

What Patterns Cannot Do

A few restrictions prevent the directive from reaching outside the package directory or surprising you with platform differences:

  • Patterns must match files in the same package's directory tree. The pattern cannot start with /, contain .., or escape upward in any way. //go:embed ../../shared/config.json fails to build with pattern ../../shared/config.json: invalid pattern syntax.
  • Patterns do not follow symbolic links that point outside the package directory. If a symlink inside the tree points to a path under the package root, the link target is embedded as data. A symlink that escapes the root is a build error.
  • Patterns must match at least one file. //go:embed nonexistent.txt produces pattern nonexistent.txt: no matching files found. This rule catches typos at build time.
  • Pattern names are interpreted with forward slashes only. Even on Windows, write templates/order.html.

Using embed.FS with the Standard Library

Because embed.FS satisfies fs.FS, every standard library function that accepts an fs.FS works with embedded content without any conversion. This is the payoff for the fs.FS design.

The compiler reads the files on the left, the //go:embed directive bundles them into the binary, and the resulting embed.FS plugs into any consumer that accepts an fs.FS. The same FS value can be parsed as templates, served as static files, and read as raw bytes, without copies and without filesystem access at runtime.

Serving Static Files

A common case for an online store is a small "About Us" web page served by the same binary. http.FileServer accepts an http.FileSystem, and http.FS adapts any fs.FS (including embed.FS) to that interface.

A request to http://localhost:8080/about.html returns the embedded static/about.html, and /style.css returns the embedded CSS. The binary is fully self-contained; no static/ folder needs to be present alongside the executable in production.

Note the call to fs.Sub. The embedded FS has files at paths like static/about.html, but you want clients to fetch them as /about.html. fs.Sub returns a view of the FS rooted at the given directory, so paths inside the sub-FS are relative to that root. Without it, clients would need to request /static/about.html, which is leakier than necessary.

Parsing Templates

html/template and text/template both ship with a ParseFS function (added in Go 1.16 alongside embed) that reads templates from an fs.FS. For order confirmation emails, this means the templates ship with the binary.

templates/order-confirmation.html:

template.ParseFS looks up each pattern inside the supplied fs.FS exactly the way it would look up files on disk. The same code works whether you pass embed.FS, os.DirFS("templates"), or an in-memory fstest.MapFS, which is convenient for testing without touching the disk.

Reading Files Directly

fs.ReadFile reads any named file from an fs.FS into a byte slice. This works equally well on embed.FS:

fs.ReadFile is convenient when you have an embed.FS and just want the bytes of one file by name. For two or three files, embed.FS.ReadFile (the method on the type) does the same thing.

fs.Sub and Path Manipulation

When you embed a directory tree, the paths inside the FS are rooted at the embedding source file's directory. That means files are addressed by paths like templates/order.html, not order.html. For many consumers (especially http.FileServer), this leading directory is noise.

fs.Sub returns a new fs.FS that's logically rooted at a subdirectory of the original. Paths inside the sub-FS are relative to that root.

Assume the embedded tree contains:

The assets/templates directory is not visible from staticSub because it sits outside the sub-FS root. This is a clean way to slice an embedded tree into the pieces different consumers care about: one sub-FS for http.FileServer, another for template parsing, another for SQL migrations.

fs.Sub returns an error if the named directory doesn't exist in the parent FS. Otherwise, the returned FS is exactly as capable as the parent (still read-only for embed.FS, still readable for fs.ReadFile and fs.Glob).

Build-Time vs Runtime

Files matched by //go:embed are read by the Go compiler at build time. The resulting bytes are linked into the binary, the same way a string literal is. Once the binary is built, the contents are fixed.

Two consequences follow:

  • Editing an embedded file does not affect a running binary. The file is read from the binary's data section, not from disk. To pick up a change, rebuild and redeploy.
  • Whatever was at the matched path at build time is what ships. If a CI system regenerates a config file before go build runs, the regenerated version is what gets embedded.

This makes //go:embed a poor fit for content that needs to change without redeploying (use a config file on disk or a remote source for that). It's a great fit for content that ships with the binary and changes only when the code does: HTML templates, SQL migrations, default configs, embedded help text, license notices, and CLI banners.

Common Patterns

A few real-world patterns recur often.

Default Config Embedded as a Fallback

The pattern: ship a default config in the binary, but allow an external file on disk to override it. This is the smoothest first-run experience because the program works out of the box.

The embedded version is the safety net. The on-disk version wins when present.

SQL Migrations Embedded for Deployment

Schema migrations ship with the binary, so deploying a new service version automatically deploys the matching schema. No separate "copy SQL files" step in the deploy script.

A real migration runner reads each file from the embedded FS in order and applies it to the database. Library projects like golang-migrate and goose accept an embed.FS directly because they were updated for the 1.16 feature.

Welcome Banner for a CLI

Single-file string targets are perfect for embedded user-facing text. The CLI ships with its welcome screen baked in.

The whole "load this asset" subsystem collapses into one variable declaration.

HTML Templates and Static Assets for a Web Server

Combining everything: one binary serves a small AlgoMart "About Us" page plus order confirmation rendering, all driven from embedded files.

templates/order.html:

Visiting /order renders the template. /static/style.css and /static/about.html come from the embedded static directory. The deployment artifact is one binary; the asset folders don't have to be present on the server.

Common Bugs

A few mistakes come up frequently with //go:embed. They're all caught at build time, but the error messages are sometimes cryptic.

Forgetting to Import "embed"

The most frequent mistake. The directive looks like a normal Go comment, so it's easy to think it stands on its own.

Build error:

Fix: Add import _ "embed" for string and []byte targets, or import "embed" for embed.FS targets.

Using the Wrong Import Form

For an embed.FS target, the embed package is used by name (the type embed.FS), so the import must be the normal form, not the blank form.

Build error:

Fix: Drop the blank, since embed.FS is used:

Pattern Matches No Files

//go:embed rejects patterns that don't match anything. This is the compiler's way of catching typos and stale references.

Build error:

Fix: Correct the filename, or make sure the file is in the package directory tree.

Using .. in a Pattern

Patterns are restricted to the package directory and its subdirectories.

Build error:

Fix: Move the file inside the package, or copy it during the build (with go generate) so the embedded copy sits next to the source. The directive deliberately refuses to reach outside the package because doing so would tie the build to the directory layout above the package.

Blank Line Between Directive and Variable

The //go:embed directive must be immediately above the variable declaration. A blank line breaks the association.

Build error:

Fix: Remove the blank line so the directive sits flush against the var.

Embedding into a Local Variable

The directive only works on package-level variables.

Build error:

Fix: Move the variable out of the function. Package-level is the only valid scope.

Quiz

embed Directive Quiz

10 quizzes