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

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!

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

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.

Related lessons