skip to content

What does the //go:embed directive do, and what must be true of the declaration it sits on?

level: juniorimportance: must knowfreq 58%

answer

  1. the go command reads a comment
  2. one variable, three allowed types
  3. the import is needed even when unused
  4. patterns cannot climb out of the package
  5. no match is a build failure

basics

~20 s

The //go:embed directive copies the files matching its pattern into the compiled binary at build time. It must sit directly above one package-level variable of type string, []byte, or embed.FS, and the source file must import the embed package.

solid answer

~50 s

`//go:embed` is a directive read by the `go` command, not a runtime call: while building, the toolchain reads the files matching the patterns and bakes their bytes into the executable, so the program no longer needs those files on disk. The directive must immediately precede the declaration of a single package-level variable — only blank lines and other line comments may sit between them — and that variable must be of type `string`, `[]byte`, or `embed.FS`. The file must import `embed`; when the variable is a `string` or `[]byte` the package name is never mentioned, so you need a blank import, `import _ "embed"`. Patterns use `path.Match` syntax, are relative to the directory of the source file, may not contain `..` elements or begin with a slash, and may not reach outside the package. A pattern matching nothing is a build error, not an empty variable.

code

go · 10 lines
go
import "embed"

//go:embed version.txt
var version string

//go:embed banner.txt
var banner []byte

//go:embed templates/*.html
var templates embed.FS

go deeper

for a junior

Be ready to write the three lines from memory: the import of embed, the directive comment with no space after the slashes, and a package-level var of type string, []byte or embed.FS.

for a middle

Explain that the go command resolves the patterns at build time, and state the pattern rules: path.Match syntax, relative to the source file's directory, no .. elements and no leading slash.

for a senior

Point out what this buys a pipeline: a mistyped or missing asset breaks the build on the machine that builds it, so it can never reach production as a blank page at run time.

for a principal

Own the convention for where embeddable assets live in the tree, since the no-parent-directory rule means package layout and asset layout have to be decided together.

## What the directive actually is `//go:embed` is a *compiler directive*: a line comment with no space after the `//` that the `go` command interprets while building the package. Nothing about it happens at run time. When the toolchain sees the directive it resolves the patterns against the files sitting in the package's own directory on the build machine, reads their contents, and writes those bytes into the object file that becomes part of the final executable. The resulting binary is self-contained: you can delete the whole source tree, copy the executable to an empty container, and the data is still there. ## The declaration it must sit on Three rules govern the declaration, and each has a distinct failure mode. **One variable, at package scope.** The directive applies to the very next variable declaration in the file. Only blank lines and other line comments may separate them. It cannot be attached to a variable declared inside a function — the `go` command rejects that outright — and it cannot be attached to a grouped declaration that declares more than one name. **One of three types.** The variable must be declared as `string`, `[]byte`, or `embed.FS`. For `string` and `[]byte` the directive must match *exactly one* file, and the variable holds that file's contents. For `embed.FS` the directive may match any number of files and directories, and the variable behaves as a small read-only filesystem with `Open`, `ReadFile`, and `ReadDir` methods. **The import.** The file must import `embed`. When the variable is declared as `embed.FS` the import is ordinary, because you name the package in the type. When the variable is a `string` or a `[]byte` you never write `embed.` anywhere, so the compiler would flag the import as unused — that is why the blank form `import _ "embed"` exists. Leaving it out produces an error telling you that `//go:embed` is only allowed in files that import `embed`. ## The pattern rules Patterns use `path.Match` syntax — `*`, `?`, and character classes, always with forward slashes, on every operating system. They are interpreted relative to the directory containing the source file that carries the directive. Several consequences follow: - A pattern may not contain a `.` or `..` path element, and may not begin or end with a slash. There is deliberately no way to reach into a sibling directory or up to the repository root: everything a package embeds must live inside that package's directory tree. - A single directive may list several space-separated patterns, and a single variable may carry several directives; all of the matches are combined into one `embed.FS`. - If a pattern names a directory, the whole subtree under it is embedded recursively — except that names beginning with `.` or `_` are skipped unless the pattern is written with the `all:` prefix. - A pattern that matches nothing is a **build error**. This is a deliberate design choice: a typo in an asset path fails the build on the developer's machine or in the pipeline rather than producing a binary that serves blank pages. ## The gotcha that produces no error at all Because the directive is a comment, writing `// go:embed assets` with a space is simply an ordinary comment. The code still compiles, the variable keeps its zero value — an empty string, a nil slice, or an `embed.FS` containing nothing — and the failure only shows up when something asks for a file. The same is true of a directive separated from its declaration by a non-comment line. When an embedded asset is missing, checking the exact spelling and position of the directive is the first thing to do. ## Why it exists Before this directive, shipping templates, SQL migrations, or a web UI with a Go program meant either a code generator that turned files into a giant Go source file, or a deployment step that copied a directory next to the binary. Both were error-prone: the generated file drifted from the originals, and the copied directory could be missing, stale, or a different version from the binary. Embedding makes the assets part of the same immutable artefact as the code that reads them, which removes an entire class of version-skew incident. The cost, paid at build time, is a larger executable and a rebuild for every asset change.

  • Why does a variable declared as a plain string still need a blank import of embed?
    The directive is only honoured in files that import `embed`. When the variable is a `string` or `[]byte` you never mention the package in the code, so an ordinary import would be reported as unused; `import _ "embed"` records the dependency without naming it. Omitting it fails the build with a message saying `//go:embed` is only allowed in files that import `embed`.
  • Can the directive be attached to a variable inside a function?
    No. It applies only to a package-level variable declaration. The `go` command rejects a directive on a local variable, because the data has to be laid out in the binary at build time rather than created per call. If a function needs the bytes, declare the package-level variable and read from it.
  • What happens if you write several patterns, or several directives on one variable?
    Both are allowed for an `embed.FS`: one directive may list space-separated patterns, and a variable may carry several directive lines. All matches are unioned into the same filesystem. For a `string` or `[]byte` variable the rules are stricter — the patterns together must match exactly one file.

It is closer to a build step that pastes a file into your source than to opening a file: by the time the program runs, the bytes are already part of the executable.

saying these in an interview costs you the question

  • Says the files are read from disk when the program starts
  • Puts the directive on a local variable inside a function
  • Assumes a pattern that matches nothing yields an empty value
  • Forgets the embed import when the variable is a string
  • Believes a ../ pattern is fine as long as the file exists
  • Writes a space after the slashes and wonders why it is empty