Go generate code generation

Go code generation is handled by the `//go:generate` directive, which allows you to define shell commands that run via `go generate` to create source files from templates or data.

The missing piece in your build pipeline

Picture a project where you need to convert a list of database columns into Go struct fields. Or a service that requires mock implementations for every interface you define. Writing that boilerplate by hand is tedious. Copying and pasting it creates drift. You need a way to tell the toolchain to build the missing pieces from a template, and keep the repository clean.

Go handles this with the //go:generate directive. It is not a compiler feature. It is a declarative instruction that tells a separate tool to run a shell command. The command produces Go source files. Those files live in your directory alongside your handwritten code. The build system treats them exactly like any other .go file, but it refuses to compile them unless they already exist on disk.

Think of it like a recipe card taped to a blueprint. The blueprint shows the final structure. The recipe card tells you how to mix the concrete before you start laying bricks. You run the recipe step first. Once the concrete is poured and set, you follow the blueprint. The builder never mixes concrete during the inspection phase.

What go generate actually does

The directive lives inside a regular Go source file. It always starts with //go:generate followed by a shell command. The command runs in the same directory as the file containing the directive. When you run go generate in your terminal, the tool scans every .go file in the current package. It extracts every directive, executes the commands in order, and exits. That is the entire job.

The separation between generation and compilation is intentional. Go does not run go generate automatically during go build or go test. This design choice keeps builds fast and deterministic. If generation ran automatically, every compilation would trigger external tools, network calls, or template rendering. Build times would spike. Reproducibility would vanish. By forcing you to run generation explicitly, Go guarantees that the source tree matches exactly what the compiler sees.

The tool parses directives using a simple regular expression. It looks for lines that start with //go:generate. It strips the prefix, trims whitespace, and passes the remainder to the system shell. Arguments are split by spaces. Quoted strings stay intact. You can chain commands with && or ; if your shell supports it. The tool does not evaluate Go expressions. It does not resolve package paths. It only runs what you write.

A minimal example

Start with a simple constant that you want to generate from a text file. Create a file named version.go in your package.

// version.go
package main

//go:generate echo 'const Version = "1.0.0"' > version_generated.go

// PrintVersion outputs the generated version string to stdout.
func PrintVersion() {
    // This call requires version_generated.go to exist at compile time.
    // The compiler will reject the program if the file is missing.
    println(Version)
}

The directive tells the shell to write a single line into version_generated.go. Run go generate in the terminal. The tool finds the directive, executes the echo command, and creates the file. Your directory now contains version.go and version_generated.go. Run go build and the program compiles. The generated constant is treated as a first-class citizen.

How the compiler and runtime interact with generated code

The compiler does not know the difference between handwritten and generated files. It only cares about valid syntax, correct imports, and type safety. When go build starts, it reads every .go file in the package. It parses them, resolves symbols, and links them into a binary. If version_generated.go is missing, the compiler stops immediately. It rejects the program with undefined: Version because the symbol was never declared in any available source file.

This strictness is a feature. It forces you to treat generated code as part of the contract. You cannot ship a binary that depends on a file that might not exist. The build fails fast. You fix the generation step. The repository stays consistent.

At runtime, generated code behaves identically to handwritten code. Constants are baked into the binary. Functions are compiled into machine instructions. Channels and goroutines created in generated files follow the same scheduling rules. The Go runtime does not track provenance. It only executes instructions.

The module system also treats generated files normally. go mod tidy will add any imports that the generated code requires. go list will include the package in dependency graphs. The toolchain assumes the file belongs to the module. You do not need special build tags or ignore patterns. The file is just another source file.

Real-world usage: mocks and templates

Most teams use go generate for interfaces, protobufs, or string conversions. External tools like mockery, stringer, or protoc-gen-go read your source files and emit new ones. The directive becomes a bridge between your code and those tools.

Here is how you would generate a mock for a service interface.

// service.go
package service

//go:generate mockery -name=UserStore -output=./mocks -outpkg=mocks

// UserStore defines the contract for retrieving user data.
type UserStore interface {
    // GetUser fetches a user by their unique identifier.
    // The mock implementation will record calls and return configurable values.
    GetUser(id string) (*User, error)
}

Running go generate invokes mockery. The tool reads the UserStore interface, generates a mock implementation, and writes it to mocks/mock_user_store.go. The mock file contains methods that record calls and return configurable values. You import the mock package in your tests. You verify behavior without touching a real database.

The community follows a strict convention for generated files. Every generated file must start with a comment that reads // Code generated by <tool>. DO NOT EDIT. This comment signals to linters, formatters, and other developers that the file is machine-produced. Tools like gofmt and go vet respect the comment. They skip formatting checks and style warnings on the file. You do not argue indentation with a machine. You let the generator handle it.

Another convention is the receiver name. Generated methods usually follow the same pattern as handwritten code. The receiver name is one or two letters matching the type. (m *MockUserStore) GetUser(...) keeps the signature predictable. You do not use this or self. The language does not require a specific name, but the ecosystem expects consistency.

Pitfalls and compiler errors

The most common mistake is forgetting to run generation before committing. If you push a file that references a generated constant, your teammates pull the code and run go build. The compiler rejects the program with undefined: Version. Their build breaks. The fix is simple. Commit the generated files alongside the source files. Treat them as part of the repository. Add a pre-commit hook or a CI step that runs go generate ./... and checks for diffs. If the diff is empty, the generation step is up to date.

Another trap is relying on environment variables or network calls inside the directive. The directive runs in the developer's shell. It inherits their environment. If the command depends on a specific version of a tool or a local configuration file, it will fail on a different machine. Keep directives deterministic. Use absolute paths for output directories. Pin tool versions in a tools.go file or a go.mod directive. The build should work on a fresh clone without manual setup.

You might also run into import cycles if you generate code that imports the same package. The compiler catches this early. It rejects the program with import cycle not allowed. The solution is to place generated code in a separate package or use an interface to break the dependency. Trust the type system. Wrap the value or change the design.

Debugging generation failures requires reading the shell output. go generate does not swallow errors. It prints the command it ran, then prints the exit code. If the tool crashes, you see the panic. If the tool exits with a non-zero status, you see the error message. The tool does not retry. It stops at the first failure. You fix the command. You run it again.

When to use go generate

Use //go:generate when you need to run external tools during development to produce Go source files. Use a Makefile or CI script when you need to orchestrate multiple generation steps across different packages. Use standard library functions or runtime configuration when the data changes frequently and does not belong in compiled code. Skip code generation entirely when a simple loop or map solves the problem without extra tooling.

Where to go next