Code Generation and Embedding

Updated

September 13, 2026

Code Generation and Embedding

The boring default for files the binary must always have (banners, small HTML, default copy) is //go:embed. The boring default for code you are tempted to generate is do not generate it until a human-written file is worse. When you do generate, go generate is a documented go run of a helper you own — not a zoo of plugins.

Mental model

go:embed is a compiler feature. You write a directive above a string, []byte, or embed.FS variable. The file contents are baked into the binary at compile time. There is no extra tool.

go generate is not a compiler feature. It is a convention: comments of the form //go:generate command are run by go generate. The command can be anything. The boring command is go run some_helper.go with a //go:build ignore tag so the helper is not part of your program.

Prefer embed. Reach for generate when the generated Go is mechanical (tables, stringers you would otherwise mistype) and the helper is small enough to read.

Worked examples

Create an empty directory. Save go.mod:

module example.com/desk

go 1.27

Case 1: Embed a text file as a string

Create hello.txt next to main.go. The compiler only embeds files in the same package directory (or a subdirectory you name). This book does not ship the file; you type it.

hello.txt (include the trailing newline):

desk is open

Save as main.go:

// main.go
package main

import (
    _ "embed"
    "fmt"
)

//go:embed hello.txt
var greeting string

func main() {
    fmt.Print(greeting)
}

The blank import of embed is required even though you never mention the name embed. The directive is what the compiler looks for.

Run (from the directory that contains go.mod, main.go, and hello.txt):

go run .

Output:

desk is open

fmt.Print does not add a newline. The line break you see comes from the file.

Case 2: Embed the same file as bytes, then as a file system

Bytes are useful when the file is not valid UTF-8 or when you will pass it to something that wants []byte. embed.FS is useful when you have several files and want ReadFile.

Save as main.go (keep hello.txt and go.mod):

// main.go
package main

import (
    "embed"
    "fmt"
    "os"
)

//go:embed hello.txt
var greeting []byte

//go:embed hello.txt
var files embed.FS

func main() {
    fmt.Printf("bytes: %q\n", greeting)
    b, err := files.ReadFile("hello.txt")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("fs: %q\n", b)
}

Run:

go run .

Output:

bytes: "desk is open\n"
fs: "desk is open\n"

If your hello.txt has no newline at the end, the \n inside the quotes will be missing. Match the file you actually saved.

The path in ReadFile is relative to the package directory, using slash separators, and must match the embed pattern.

Case 3: go generate with a helper you can read

Embed is enough for banners. Generation is for Go source you do not want to type. Here the helper writes banner.go with a single constant. No extra module, no stringer install.

Save as main.go:

// main.go
package main

import "fmt"

//go:generate go run gen_banner.go

func main() {
    fmt.Println(banner)
}

Save as gen_banner.go. The ignore build tag keeps this file out of go run . and go build so you do not have two main functions:

// gen_banner.go
//go:build ignore

package main

import (
    "fmt"
    "os"
)

func main() {
    const src = "package main\n\nconst banner = \"FRONT DESK\"\n"
    if err := os.WriteFile("banner.go", []byte(src), 0o644); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

From the module directory:

go generate .

No output on success. The helper writes banner.go:

// banner.go
package main

const banner = "FRONT DESK"

Then:

go run .

Output:

FRONT DESK

If you skip go generate, go run . fails with undefined: banner. Commit banner.go if the rest of the team should build without running generate. Or run generate in CI before go test. Pick one and document it; do not require a mystery binary named stringer for a one-line constant.

The trap

Generating code because a blog showed go:generate stringer is how a desk tool grows a toolchain nobody can install on a bad wifi day.

This program does not need a generator. A typed constant is enough:

// shift.go
package main

import "fmt"

type Shift int

const (
    Morning Shift = iota
    Evening
)

func (s Shift) String() string {
    switch s {
    case Morning:
        return "morning"
    case Evening:
        return "evening"
    default:
        return fmt.Sprintf("Shift(%d)", int(s))
    }
}

func main() {
    fmt.Println(Morning)
    fmt.Println(Evening)
}

Run:

go run shift.go

Output:

morning
evening

Write the String method. When you have twenty values and they drift, then a helper like Case 3 is earned. Embed still wins for files that are not Go.

The boring rule

  • Put small static files next to the package and //go:embed them. Import _ "embed" or "embed".
  • Run go run . (a package), not a lone file list, so embed patterns resolve.
  • Do not fetch a generator you have not read. go run helper.go with //go:build ignore is the whole pattern.
  • Commit generated output or run generate in CI. Not neither.
  • Never embed secrets. Embed is for public copy and assets, not keys.

Try this

  1. Change hello.txt to two lines. Re-run Case 1. Confirm both lines appear.
  2. Add closed.txt with the text desk is closed. Embed both files with //go:embed hello.txt closed.txt into one embed.FS and print each.
  3. Point gen_banner.go at a different constant name and fix main.go to match. Run go generate . then go run ..
  4. Delete banner.go and run go run . without generate. Read the error. Restore the file with go generate ..