The build step that lives in your code
You are building a service and need a version.go file containing the current git commit hash. You write a script to generate it. You add a Makefile target. You tell the team to run make generate. A new developer clones the repo, runs go run main.go, and gets a compile error because the generated file is missing. The build is fragile. The generation step is disconnected from the code that needs it.
go generate solves this by embedding build commands directly in your source files. You add a comment like //go:generate go run gen.go to your Go file. When you run go generate, the tool scans your code, finds the comment, and executes the command. The generation step travels with the code. New developers run go generate ./... and everything works. The build becomes self-documenting and reproducible.
How go generate works
go generate is a command-line tool that looks for special comments in your Go source files. These comments start with //go:generate. The rest of the comment is treated as a shell command. The tool runs these commands in the order they appear in the file.
This is a convention, not a compiler feature. The Go compiler ignores //go:generate comments. go build does not run them. go test does not run them. You must invoke go generate explicitly. This design keeps the compiler simple and makes generation steps visible. There is no hidden magic. If code depends on a generated file, the comment makes that dependency obvious to anyone reading the source.
The command runs in the directory containing the source file. Paths in the directive are relative to that directory. This matters when you have a large project with generated files in different packages.
go generate is a convention, not a compiler pass. The compiler sees nothing.
Minimal example
Here is the simplest possible usage: a directive that runs a shell command to create a text file.
// Package main demonstrates a basic go generate directive.
package main
//go:generate echo "Generated content" > output.txt
// The directive runs echo and redirects output to a file.
// go generate executes this command when invoked.
func main() {
// The program expects output.txt to exist.
// Run go generate before running this code.
println("Check output.txt")
}
Run go generate in the directory containing this file. The tool finds the comment, strips the //go:generate prefix, and runs echo "Generated content" > output.txt. The file output.txt appears. Run go run main.go and the program works.
If you run go build without running go generate first, the build succeeds because the program does not import or use output.txt at compile time. The dependency is logical, not syntactic. The comment documents the dependency.
Walkthrough
When you run go generate, the tool performs a linear scan of the source files. It looks for line comments matching the pattern //go:generate. Block comments (/* ... */) are ignored. Only line comments work. This restriction keeps parsing simple and prevents accidental directives in documentation blocks.
The tool extracts the command string and executes it using the system shell. Arguments are passed directly. If you need to pass arguments with spaces, quote them in the comment. The shell handles the quoting.
//go:generate mytool --name "My Service" --version 1.0.0
// Quotes protect arguments with spaces from shell splitting.
The command runs in the directory of the source file. If your directive references a file, the path is relative to that directory. This behavior ensures that generation steps are portable. Moving a package to a different directory does not break relative paths.
You can run go generate ./... to process all packages in the current module. The tool descends into subdirectories and runs directives in every package. This is the standard way to regenerate all generated code in a project.
go generate runs commands. It does not check their success. If a command fails, go generate prints an error and stops. It does not retry.
Realistic example: Version generation
A common use case is generating a version.go file from git metadata. You want the version string to update automatically when you tag a release. You can write a small Go program to read the git output and write the Go source file.
Here is the main package with the directive.
// Package main uses go generate to inject version info.
package main
//go:generate git describe --tags --always > version.txt
//go:generate go run gen_version.go
// The first directive captures the git tag or hash.
// The second directive runs the generator script.
// Directives run in order, so version.txt exists before gen_version.go runs.
// Version holds the git tag or commit hash.
var Version string
func main() {
// Print the version at runtime.
println("Version:", Version)
}
The generator script reads version.txt and writes version.go. The script must not be compiled into the main binary. You use a build tag to exclude it.
//go:build ignore
// +build ignore
// Package main generates version.go from version.txt.
package main
import (
"fmt"
"os"
)
func main() {
// Read the raw version string from the file.
data, err := os.ReadFile("version.txt")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Write the Go source file with the version variable.
content := "package main\n\nvar Version = \"" + string(data) + "\"\n"
err = os.WriteFile("version.go", []byte(content), 0644)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The //go:build ignore tag tells the compiler to skip this file. Without it, gen_version.go would be compiled into the binary, causing a duplicate main function error. The tag is a convention for generator scripts. It keeps the build clean.
Run go generate. The tool runs git describe, writes version.txt, then runs go run gen_version.go. The script reads the file and writes version.go. Now go build compiles the project with the version embedded.
The generator script uses //go:build ignore to stay out of the binary. Build tags control inclusion.
Pitfalls and errors
go generate is simple, but there are common mistakes.
If you forget to run go generate, the generated files are missing. The compiler rejects the program with undefined: Version if you reference a variable from the missing file. The error looks like a normal missing-symbol error. The fix is to run go generate before building.
If your directive uses an external tool that is not installed, the command fails. The tool prints an error like exec: "stringer": executable file not found in $PATH. This happens often with tools like stringer, mockgen, or protoc. You must install these tools separately. The convention is to install them via go install. For example, go install golang.org/x/tools/cmd/stringer@latest. go generate does not fetch or install tools. It assumes the environment is ready.
If your directive has a syntax error, go generate may run the wrong command. The tool splits the command string on whitespace. If you forget quotes around an argument with spaces, the shell interprets the parts incorrectly. Test your directives manually in the shell to verify they work.
Generated files should be idempotent. Running go generate twice should produce the same result. If your generator appends to a file or creates non-deterministic output, you will see diffs in version control or inconsistent builds. Design generators to overwrite files completely.
Generated code should pass gofmt. The Go community expects all code to be formatted. If your generator outputs unformatted code, reviewers will flag it. Run gofmt on the output or use the go/format package in your generator to format the code before writing it.
gofmt is mandatory. Generated code must be formatted.
If you use go generate ./..., the tool runs directives in all packages. This can be slow if you have many packages or heavy generators. Use go generate ./pkg/... to target specific packages during development.
The worst generator bug is the one that produces valid code with wrong logic. go generate cannot check the semantics of the output. You must test generated code like any other code.
When to use go generate
Choosing the right mechanism for code production depends on the complexity and frequency of the task.
Use go generate when you need to produce Go source files from templates, external data, or repetitive patterns. Use go generate when the generation step is tightly coupled to a specific package and should live in the same directory. Use go generate when you want new developers to regenerate code by running a single command that scans the whole project.
Use a build tag (//go:build) when you want to include or exclude files based on environment or platform, not generate new ones. Build tags are for conditional compilation, not code production.
Use a Makefile or CI script when the generation step requires complex orchestration, parallel execution, or non-Go tooling that does not fit in a comment. Makefiles are better for high-level build workflows.
Use runtime initialization when the data can be computed at startup without the overhead of a build step. Runtime computation avoids generation entirely if the data is dynamic or cheap to calculate.
Use go run directly when you need a one-off script that does not produce artifacts for the build. Scripts are for tasks, not for generating source files consumed by the compiler.
Generate code when the pattern repeats. Write code when the logic is unique.