skip to content

When should a Go service embed its assets in the binary instead of shipping them beside it?

level: principalimportance: nice to knowfreq 32%

answer

  1. who is allowed to change the bytes
  2. one artefact, one rollback
  3. the fix path is the tradeoff
  4. size and build time are the bill
  5. the exported parameter type outlives both

basics

~20 s

Embed when you want one artefact whose code and assets cannot drift apart and can accept that every asset change means a rebuild and redeploy. Keep files outside when a non-engineering owner must change them. Exported APIs should take an fs.FS.

solid answer

~50 s

The real question is who is allowed to change the assets and on what timescale. Embedding with `//go:embed` makes templates, migrations, and static files part of the same immutable artefact as the code that reads them, which removes version skew between binary and assets entirely and makes rollback one operation. The price is paid in the release path: a one-line copy fix now needs a build, a review, and a deploy, and nobody can patch a template on a host at three in the morning. If that operational posture is not one your organisation wants, the assets belong outside the binary and behind a config volume or an object store, and the deployment has to guarantee they match. The decision that outlives both is the API: a package that exports `New(fsys fs.FS)` lets each consumer choose embedded, on-disk, or a re-rooted subtree, while `New(dir string)` forces disk on everyone and cannot be widened later without breaking importers.

code

go · 5 lines
go
// Forces the caller to have a real directory, forever.
func New(dir string) (*Renderer, error)

// Lets the caller pass an embed.FS, os.DirFS, or a sub-view.
func New(fsys fs.FS) (*Renderer, error)

go deeper

for a junior

Know what embedding means in practice: the files travel inside the executable, so there is nothing to copy alongside it and no working directory to get right.

for a middle

Be able to list the concrete tradeoffs — build-time resolution, artefact size, and a rebuild plus redeploy for every asset change — rather than picking a side.

for a senior

Show how the choice changes incident response: an embedded asset cannot be edited on a host, so a one-word fix becomes a full release, and that has to be acceptable before you choose it.

for a principal

Own both halves of the decision and its blast radius: the distribution posture for the organisation, and the exported parameter type, which other teams inherit and cannot renegotiate later.

## The decision is about the release path, not about file loading Both options work. Both are one line of code. What differs is who is able to change the bytes, and how long that takes. Framing it as a technical preference is the mistake; framing it as a question about the deployment and incident-response model produces a defensible answer. ## What embedding buys **No version skew.** The most common asset incident is a binary rendering a template from a different release — a half-updated volume, a cached layer, a config map applied out of order. When the template lives in the binary, that failure mode does not exist. The code and the file it reads are the same artefact and version together. **One thing to ship, one thing to roll back.** A single executable can be copied into an empty container, signed as a unit, and reverted by pointing at the previous build. There is no second distribution channel to keep in step. **Build-time verification.** Pattern resolution happens on the build machine, and a pattern matching nothing fails the build. A missing asset becomes a red pipeline rather than a blank page in production. **Simpler local runs.** The binary works from any working directory, which removes a whole genre of "it works from the repository root" bug. ## What embedding costs **Every change is a release.** Fixing one word of copy takes the full path: commit, review, build, deploy. For an organisation where marketing or support owns that copy, that is the wrong owner and the wrong latency, and the answer is a content store, not an embedded file. **No host-level remediation.** On call at three in the morning, nobody can edit a file to unblock a customer. This is often stated as a benefit — immutability — and it genuinely is, but it should be a chosen posture rather than a surprise. **Artefact size and build time.** The bytes live in the executable and are paged in on demand, so a large tree grows the artefact and image-pull time more than it grows steady-state memory. Media, sample data, and vendored front-end bundles are the usual offenders, and the discipline is to embed what the program cannot run without and keep the rest outside. **The no-parent-directory rule shapes your layout.** Since a pattern cannot climb out of the package directory, assets must live inside the package that embeds them. That is a real constraint on repository structure, and it is best decided once for the codebase rather than negotiated per package. ## The API decision, which is harder to reverse For a package other teams import, the more consequential choice is the parameter type. `func New(dir string)` hard-codes the assumption that files are on disk and that the caller knows a path. `func New(fsys fs.FS)` accepts an `embed.FS`, the result of `os.DirFS`, or a re-rooted sub-view, and costs the caller nothing extra. It also keeps the *distribution* decision with the consumer, which is exactly where it belongs — your library cannot know whether the importing service ships a single binary or mounts a volume. Because an exported signature is the part of the design you cannot change quietly, this is worth getting right before the first release even when the immediate caller only ever passes one thing. ## How to defend the call A good answer names the constraint that decides it rather than declaring a universal rule. Templates and SQL migrations that only engineers change, on a service that deploys several times a day: embed, and say so because the deploy cadence already makes the release path cheap. User-editable content, or a service that deploys monthly through a change board: keep it outside, and invest in making the deployment guarantee that binary and assets match. Reusable packages: take an `fs.FS` and refuse to make the decision on the consumer's behalf. And in either case, if the assets are embedded, add a test that walks the filesystem and asserts the critical entries exist, so the artefact cannot ship short.

  • Why should a reusable package take an fs.FS rather than a directory name?
    Because the package cannot know how the importing service is distributed. An `fs.FS` parameter accepts an `embed.FS`, a directory via `os.DirFS`, or a re-rooted subtree, so every consumer keeps its own choice; a directory string forces disk on all of them. It is also the harder decision to reverse, since changing an exported signature breaks importers.
  • How do you stop an embedded asset tree from bloating the binary?
    Embed what the program cannot start without — templates, migrations, small static files — and keep media, sample data, and large bundles in an object store or a separate image layer. Prefer directory patterns scoped to what ships rather than a whole build-output tree, and watch artefact size in the pipeline so growth is visible when it happens rather than at image-pull time.
  • Operations wants to hot-fix a template on a running host. What do you tell them?
    With embedded assets that is not possible by design, and the honest answer is to make the release path fast enough that it is not needed — or to move that specific content out of the binary if a non-engineering owner genuinely needs to change it. What you should not do is add a runtime override path that reads from disk when a file happens to exist, because it reintroduces exactly the skew embedding removed.
  • The assets stay outside the binary. What do you now owe the deployment?
    A guarantee that the binary and the asset set match: version them together, ship them in the same image or bundle, and have the program verify at startup that what it found is the expected version rather than discovering a mismatch on the first request. The work that embedding did for free becomes deployment work you have to do explicitly.

saying these in an interview costs you the question

  • Treats embedding as always correct with no cost stated
  • Ignores that every asset change becomes a full release
  • Exports a directory-name parameter from a reusable package
  • Adds a disk override that silently shadows embedded files
  • Embeds large media without measuring artefact growth