Embedding Files & Build Tags
Reviewed & published by Brayan K
The //go:embed directive bakes files and directories straight into your compiled binary so you can ship a single self-contained executable, while build tags (//go:build) decide which source files are compiled for which platform.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Embed a file into a string or []byte
- Embed directories into an embed.FS
- Read and walk an embedded filesystem
- Serve embedded static files over HTTP
- Write build constraints with //go:build
- Compile different files per platform
1️⃣ Embedding with //go:embed
Put //go:embed <path> on the line directly above a package-level variable and import embed. A string or []byte variable receives a single file; an embed.FS receives whole directories as an in-memory filesystem you can ReadFile and fs.WalkDir. This example assumes a project with version.txt and a static/ folder next to it.
package main
import (
"embed"
"fmt"
"io/fs"
)
// Embed a single file into a string. The directive must sit on the line
// DIRECTLY above the variable, with no blank line between.
//go:embed version.txt
var version string
// Embed a whole directory tree into an embed.FS (an in-memory filesystem).
//go:embed static
var staticFiles embed.FS
func main() {
// The embedded string is available at runtime with NO file on disk.
fmt.Printf("version: %s", version)
// Read one embedded file out of the FS.
data, _ := staticFiles.ReadFile("static/index.html")
fmt.Printf("index.html: %s", data)
// Walk the embedded tree to list what got bundled.
fmt.Println("bundled files:")
fs.WalkDir(staticFiles, "static", func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() {
fmt.Println(" -", path)
}
return nil
})
}2️⃣ Serving an embedded site
Because embed.FS implements the standard fs.FS interface, you can serve it directly: wrap it with http.FS and hand it to http.FileServer. Your entire front-end ships inside one binary. We exercise it with httptest so you can see a real response.
package main
import (
"embed"
"fmt"
"io"
"net/http"
"net/http/httptest"
)
//go:embed static
var staticFiles embed.FS
func main() {
// http.FS adapts an embed.FS to http.FileSystem so FileServer can serve
// it. The whole site ships INSIDE the binary — no files to deploy.
srv := httptest.NewServer(http.FileServer(http.FS(staticFiles)))
defer srv.Close()
resp, _ := http.Get(srv.URL + "/static/index.html")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("GET /static/index.html -> %d\n%s", resp.StatusCode, body)
}3️⃣ Build constraints (//go:build)
A //go:build line at the top of a file (above package, followed by a blank line) controls when that file is compiled. Split a platform-specific symbol across files: the toolchain includes exactly the ones whose constraints match the target GOOS. Here three files define a single program; on Linux only greet_linux.go is compiled.
// main.go — compiles on every platform
package main
import "fmt"
func main() {
// The compiler picks exactly ONE of the platform files below based on
// the build constraint, so 'platform' resolves at COMPILE time.
fmt.Println("built for:", platform)
}// greet_linux.go — included ONLY when building for linux
//go:build linux
package main
const platform = "linux"// greet_other.go — included on every NON-linux platform
//go:build !linux
package main
const platform = "other"🎯 Your Turn
Embed a single file into a string and print it. Fill in the two blanks marked ___, create banner.txt next to the file, then run it.
// Assume banner.txt sits next to this file and contains: Hello, Go!
package main
import "fmt"
// 🎯 YOUR TURN — fill in the blanks marked ___
// 1) Add the embed directive that loads banner.txt into the variable below.
// (It goes on the line directly above the var.)
___ // 👉 //go:embed banner.txt
var banner string
func main() {
// 2) Print the embedded contents
fmt.Print(___) // 👉 banner
// ✅ Expected output:
// Hello, Go!
}Common Errors (and the fix)
❌ Blank line between //go:embed and the var — "directive must be followed by a Go variable".
✅ Keep them on adjacent lines.
❌ Embedding without import "embed" — won't compile.
✅ Import it (use _ "embed" for the string/[]byte forms).
❌ Embed path points outside the package directory (e.g. ../assets) — not allowed.
✅ Embed only files at or below the package's own directory.
❌ //go:build not at the top (or no blank line after it) — the constraint is ignored.
✅ First non-comment lines of the file, then a blank line, then package.
🧠 Quick Recall
embed one file -> string or embed.FS?
string (or []byte) for a single file; reach for embed.FS when bundling multiple files or whole directories.
//go:build linux — on Windows the file is...?
Skipped entirely — it's not compiled into a Windows build, so a matching //go:build !linux file supplies the symbol instead.
🎯 Mini-Challenge: Bundle a Templates Folder
Add the //go:embed templates directive over the embed.FS, then ReadFile both templates and print them.
package main
import (
"embed"
"fmt"
)
// Assume a directory templates/ next to this file containing:
// templates/welcome.txt -> Welcome!
// templates/bye.txt -> Goodbye!
// 🎯 MINI-CHALLENGE
// 1) Add a //go:embed directive that bundles the whole templates directory
// into the embed.FS variable below.
// 2) In main, ReadFile both templates and print their contents.
//
// ✅ Expected output:
// Welcome!
// Goodbye!
var tmpls embed.FS
func main() {
// your code here: tmpls.ReadFile("templates/welcome.txt"), etc.
fmt.Println("embed.FS ready")
}🎉 Lesson Complete!
- ✅ //go:embed bakes files into the binary — directly above the var
- ✅ string/[]byte for one file; embed.FS for directories
- ✅ Serve embedded assets with http.FileServer(http.FS(fs))
- ✅ //go:build picks which files compile per platform
- ✅ One symbol can be split across constrained files with no conflict
Practice quiz
What does the //go:embed directive do?
- Imports a package
- Runs code at startup
- Copies a file or directory into the binary at build time
- Downloads files at runtime
Answer: Copies a file or directory into the binary at build time. //go:embed bakes the contents of a file or directory into the compiled binary at build time.
Where must //go:embed be placed relative to the variable?
- On the line directly above the variable, with no blank line
- Anywhere in the file
- Inside main
- At the top of the file
Answer: On the line directly above the variable, with no blank line. The directive must sit on the line directly above the package-level variable, with no blank line between.
Which import is required to use //go:embed?
- os
- io/fs
- bytes
- embed
Answer: embed. You must import "embed" (a blank import _ "embed" works for the string/[]byte forms).
Which variable type should you use to embed a SINGLE file's text?
- embed.FS
- string or []byte
- map[string]string
- io.Reader
Answer: string or []byte. A string or []byte receives a single file; use embed.FS for multiple files or directories.
What type bundles a whole directory tree of files?
- embed.FS
- []string
- os.File
- fs.DirEntry
Answer: embed.FS. embed.FS gives you an in-memory filesystem you can ReadFile, Open, and fs.WalkDir over.
How do you serve an embed.FS over HTTP?
- http.ServeFile(fs)
- fs.Serve()
- http.FileServer(http.FS(staticFiles))
- embed.Serve(fs)
Answer: http.FileServer(http.FS(staticFiles)). Wrap the embed.FS with http.FS to get an http.FileSystem, then pass it to http.FileServer.
What does a //go:build line at the top of a file control?
- The package name
- When (on which platform) that file is compiled
- Runtime behavior
- The import order
Answer: When (on which platform) that file is compiled. A //go:build constraint tells the toolchain when to include the file, e.g. only on a given GOOS.
A file with //go:build linux is compiled into a Windows build...
- Always
- Only with a flag
- Only if it has no errors
- Never — it is skipped
Answer: Never — it is skipped. On Windows the constraint isn't satisfied, so the file is skipped entirely; a //go:build !linux file supplies the symbol.
Can you embed a file outside the package directory, like ../assets?
- Yes
- No — only files at or below the package directory
- Only with embed.FS
- Only on Linux
Answer: No — only files at or below the package directory. Embed paths must be at or below the package's own directory; pointing outside it (e.g. ../assets) is not allowed.
Why must a //go:build line be followed by a blank line before package?
- For readability only
- To separate imports
- Otherwise the constraint is treated as an ordinary comment and ignored
- It is not required
Answer: Otherwise the constraint is treated as an ordinary comment and ignored. The constraint must be at the top followed by a blank line before package, or it is ignored as a regular comment.
Continue this course
- Previous: Generic Constraints & Type Sets
- Next: Checkpoint: Building Services — Combine routing, middleware and JSON into a tested handler — then a quiz
Frequently asked questions
What does //go:embed do, exactly?
It's a compiler directive that copies the contents of a file or directory INTO your binary at build time. You write //go:embed <path> on the line directly above a package-level variable, and import the embed package. The variable can be a string or []byte for a single file, or an embed.FS for one or more files and directories. At runtime there's no separate file to ship — it's baked in.
Why must the directive be directly above the variable?
//go:embed is a magic comment the compiler associates with the very next variable declaration. A blank line or any other statement between them breaks that association and you'll get an error like 'go:embed directive must be followed by a Go variable'. It also requires that you import "embed" even if you only use a string variable (an unnamed import _ "embed" works for the string/[]byte forms).
When should I embed a string/[]byte versus an embed.FS?
Use a string or []byte for a single file — a version string, an SQL migration, one template. Use embed.FS when you want to bundle multiple files or an entire directory tree, because embed.FS gives you an in-memory filesystem you can ReadFile, Open, and fs.WalkDir over, and it plugs into http.FS for serving static assets.
How do I serve embedded static files over HTTP?
Wrap the embed.FS with http.FS to turn it into an http.FileSystem, then hand that to http.FileServer: http.FileServer(http.FS(staticFiles)). Now your CSS, JS, and HTML are served straight from the binary, so deploying is a single self-contained executable with nothing else to copy alongside it.
What are build constraints (build tags)?
A build constraint is a //go:build line at the very top of a file (above the package clause, followed by a blank line) that tells the toolchain WHEN to include that file. //go:build linux compiles the file only on Linux; //go:build !windows excludes Windows; you can combine them with &&, ||, and !. It's how the standard library provides one API with different platform-specific implementations.
How does Go choose between platform-specific files?
For a given GOOS/GOARCH, the compiler includes only the files whose build constraints are satisfied and skips the rest. So greet_linux.go (//go:build linux) and greet_other.go (//go:build !linux) define the same symbol, but exactly one is compiled into any build — there's no conflict. Go also recognises filename suffixes like _linux.go or _windows.go as implicit constraints.