skip to content

A Go binary's //go:embed templates tree is missing _partials/header.html at runtime. Why, and how do you confirm what was embedded?

level: seniorimportance: nice to knowfreq 28%

answer

  1. look at the first character of the name
  2. the build had no reason to complain
  3. an empty pattern fails, a partial one does not
  4. three letters and a colon fix the pattern
  5. walk the filesystem value itself

basics

~20 s

When a //go:embed pattern names a directory, files and directories whose names begin with a dot or an underscore are skipped from the whole subtree, with no warning. Use the all: prefix on the pattern, and walk the embed.FS with fs.WalkDir to see what is really inside.

solid answer

~50 s

Embedding a directory walks its subtree recursively but **skips every name beginning with `.` or `_`**, so `_partials` and everything under it never made it into the binary. Nothing warns you: the pattern matched other files, so the build succeeded, and the failure only shows up when `template.ParseFS` finds nothing or `ReadFile` returns a not-exist error at runtime. The fix is the `all:` prefix — `//go:embed all:templates` — which turns off that exclusion for the subtree. The way to confirm rather than guess is to walk the FS itself: `fs.WalkDir(templates, ".", ...)` printing each path, either at startup behind a debug flag or, better, in a test that asserts the files you expect are present. Worth knowing the contrast: a pattern that matches *nothing at all* fails the build with `pattern ...: no matching files found`, so only the partial, dot-and-underscore case is silent.

code

go · 14 lines
go
//go:embed all:templates
var templates embed.FS

func listEmbedded() error {
	return fs.WalkDir(templates, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() {
			fmt.Println(path)
		}
		return nil
	})
}

go deeper

for a junior

Remember the one rule that explains it: embedding a directory skips names starting with a dot or an underscore, and the all: prefix turns that off.

for a middle

Explain why the build stayed quiet — only a pattern matching nothing at all is a compile error — and show how to enumerate an embed.FS with fs.WalkDir instead of assuming its contents.

for a senior

Demonstrate the diagnosis under pressure: reason from the runtime not-exist error back to the pattern, confirm by walking the FS in the built binary, then leave behind a test that asserts the inventory so it cannot recur.

for a principal

Frame the general rule for the team: a build artefact's contents must be verifiable from the artefact, so asset trees need naming conventions and an inventory check rather than trust in a directive that fails partially and silently.

## The rule that bit you When a `//go:embed` pattern names a directory, the toolchain embeds the whole subtree rooted there — recursively — **except** that files and directories whose names begin with `.` or `_` are excluded. So: ```go //go:embed templates var templates embed.FS ``` quietly leaves out `templates/_partials/`, `templates/.keep`, and anything beneath a directory whose name starts with one of those characters. The exclusion is not arbitrary. Go's own build system has always treated directories starting with `_` or `.` as invisible to package scanning, so `//go:embed` inherited the convention, and it usefully keeps editor droppings, `.DS_Store` files and version-control metadata out of your binary by default. It becomes a trap the moment your asset tree uses those characters for real content — and `_partials`, `_layouts`, `_includes` are extremely common names in template trees, because several template ecosystems use exactly that convention for non-page fragments. ## Why nothing warned you The build failed to complain because the pattern was not empty: `templates` matched plenty of files. Go only rejects a pattern at compile time when it matches **nothing** — `pattern templates/*.css: no matching files found`. That is a genuinely useful guard for a typo'd or renamed directory, but it cannot help with a partial match, and a partial match is exactly what the dot/underscore rule produces. So the error surfaces far away from its cause: `template.ParseFS` parses the pages but not the fragments and fails on an undefined template, or `templates.ReadFile("templates/_partials/header.html")` returns an `*fs.PathError` wrapping `fs.ErrNotExist`. On a laptop, where someone may still be reading templates from disk in a dev mode, everything works; in the shipped binary it does not. That is the version of this bug that reaches production. ## The fix Prefix the pattern with `all:`: ```go //go:embed all:templates var templates embed.FS ``` `all:` means "embed this subtree including names that start with `.` or `_`". Use it deliberately, because it also stops filtering the junk the default rule was protecting you from — if the directory can accumulate editor or tooling files, keep the asset tree clean, or list the specific subdirectories you want. There is one more wrinkle worth carrying: the exclusion applies to the recursive directory walk, not to a top-level glob match. A pattern like `templates/*` explicitly matches the entries directly inside `templates`, including one named `_partials`, while `templates` alone would not. Deeper levels are still filtered. This asymmetry surprises people, and the practical advice is not to rely on it — say what you mean with `all:`. ## Confirming rather than guessing The honest diagnostic is to look inside the `embed.FS`. It implements `fs.FS`, so `fs.WalkDir` works on it directly, rooted at `"."`: ```go fs.WalkDir(templates, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { fmt.Println(path) } return nil }) ``` Run that and you are reading the truth about the binary rather than about your working directory. Two good places to put it: - **Behind a debug subcommand or flag**, so someone holding a shipped binary can dump its asset inventory without a rebuild. This is cheap and pays for itself the first time an on-call engineer asks "does this build actually contain the new migration?" - **In a test in the same package** that asserts the expected files exist. This is the version that stops the bug recurring, because the assertion fails the moment somebody adds an underscore-prefixed directory. A test is the right home for it precisely because the check is about the contents of a build artefact, and the test compiles the same package with the same directive. ## Related things that are also silently absent - **Empty directories** are not represented in an `embed.FS` at all; the filesystem holds files, and a directory with no embedded files under it simply is not there. - **Symbolic links and irregular files** are not embedded, and patterns cannot reach outside the package directory — no `..`, no absolute paths — so an asset tree shared between two packages must be embedded by a package that actually contains it, and re-exported. - **Paths keep the pattern's prefix.** `templates/index.html`, not `index.html`. A lookup failure that looks like a missing file is very often a missing prefix instead, and `fs.Sub(templates, "templates")` is the usual way to re-root the FS before handing it to something that expects paths without it. The general lesson for the person on call: for an embedded asset tree, the build's silence is not evidence. The FS is a value you can enumerate, so enumerate it.

  • You rename the directory and the build now fails with pattern ...: no matching files found. Why is that a better failure?
    Because it is a compile-time error at the site of the mistake. A pattern that matches nothing is rejected outright, so a typo or a rename is caught before anything ships. The dangerous case is the opposite one — a pattern that matches most of what you meant — because a partial match is a successful build with a hole in it.
  • How do you stop this class of bug from coming back?
    Write a test in the same package that walks the `embed.FS` with `fs.WalkDir` and asserts the files you depend on are present. It compiles against the same directive, so it fails the moment someone adds an underscore-prefixed directory or moves an asset. A debug flag that dumps the inventory from a shipped binary is a useful companion for whoever is on call.
  • Are empty directories and symlinks embedded?
    No to both. An `embed.FS` records files, so a directory with no embedded files under it does not appear; and symbolic links and other irregular files are not followed or embedded. Patterns also cannot reach outside the package directory — no `..` elements and no absolute paths — so shared assets must be embedded by a package that physically contains them.
  • Why does the lookup path start with templates/ when the directive already named that directory?
    Because names inside the FS are the matched paths exactly as written in the pattern, so `//go:embed templates` yields `templates/index.html`. A great many not-exist errors are really a missing prefix. `fs.Sub(templates, "templates")` returns an `fs.FS` rooted at that directory when you want paths without it.

saying these in an interview costs you the question

  • Assumes embedding a directory takes every file under it
  • Concludes the build would have failed if something were missing
  • Blames the deployment for a file the binary never contained
  • Adds all: everywhere without noticing it also pulls in junk files
  • Debugs by listing the working directory instead of the embed.FS