go 104 lines · 3 tabs

Serve Embedded Static Assets with embed.FS and Cache Headers in Go

Shared by codesnips Jul 2026
3 tabs
package web

import (
	"embed"
	"io/fs"
)

//go:embed dist
var distFS embed.FS

// Assets returns the embedded frontend rooted at the dist directory,
// so request paths map directly onto file names.
func Assets() fs.FS {
	sub, err := fs.Sub(distFS, "dist")
	if err != nil {
		// embed guarantees dist exists at build time; a failure here is a bug.
		panic(err)
	}
	return sub
}
3 files · go Explain with highlit

This snippet shows how a single Go binary can ship its own frontend by embedding static files at compile time with the embed package and serving them over net/http. The pattern removes the need to deploy a separate public/ directory alongside the binary, which simplifies containers and eliminates a whole class of "file not found in production" bugs.

In assets.go, the //go:embed dist directive captures the entire dist directory into an embed.FS named distFS. Because the embedded tree includes the top-level dist folder, fs.Sub is used to re-root the filesystem so paths line up with request URLs like /index.html rather than /dist/index.html. The resulting fs.FS is exposed through Assets() so the HTTP layer never touches the raw embed variable directly.

static_handler.go wraps that filesystem in a handler with real-world behavior. http.FS adapts an fs.FS into an http.FileSystem so http.FileServer can serve it. The key detail is the fallback logic: single-page apps route client-side, so any path that does not resolve to a real embedded file is rewritten to index.html before serving, letting the frontend router handle it. contentHash computes a short sha256-derived ETag per file so browsers can revalidate cheaply; hashed asset filenames get a long immutable cache lifetime while index.html is marked no-cache to avoid serving a stale shell that points at deleted bundles. This split is the standard trade-off for SPA deploys — cache the fingerprinted files aggressively, never cache the entry point.

main.go ties it together, mounting the asset handler under / while keeping an API prefix separate, and demonstrates that ordering matters: more specific mux patterns win, so /api/ is registered before the catch-all. A subtle pitfall worth noting is that embed.FS always uses forward slashes and is read-only, so callers must not rely on OS path separators or attempt writes. The approach fits any tool or service that wants a zero-dependency, self-contained deployment.


Related snips

Share this code

Here's the card — post it anywhere.

Serve Embedded Static Assets with embed.FS and Cache Headers in Go — share card
Link copied