The robot that writes the boring code
You are building a library and you need a mock implementation for testing. You write the interface. You write the mock by hand. Two weeks later, you add a method to the interface. You update the interface. You forget to update the mock. Your tests compile, but they panic at runtime because the mock doesn't implement the interface anymore. Or you are parsing a configuration format and you need to generate unmarshaling code for every field. You write a script. You run the script. You commit the output. You forget to run the script after a refactor. The generated code drifts from the source.
Code generation solves this by making the transformation explicit and repeatable. Go provides a built-in tool to orchestrate this workflow. The go generate command scans your source files for special directives and runs the commands you specify. It acts as a pre-processor that creates source files before the compiler ever sees them. The generated files are just Go code. They compile and run like everything else. The difference is that a machine wrote them, not you.
How the directive system works
The go generate tool is a text scanner. It does not parse Go syntax. It does not understand imports or types. It reads every Go source file in the current directory and looks for comments that start with //go:generate. When it finds one, it treats the rest of the line as a command to execute.
The command runs in the directory containing the source file, not necessarily the directory where you typed go generate. This distinction matters when your generator reads or writes files using relative paths. The working directory for the command is always the location of the file holding the directive.
Directives can appear anywhere in a comment block, but the community convention is to place them at the top of the file, immediately after the package clause. This keeps build dependencies visible to anyone reading the file. You can also place multiple directives in the same file. The tool runs them in the order they appear.
//go:generate go run gen_version.go
//go:generate go run gen_mock.go
package main
// Version is injected by the generator.
var Version string
The go generate command accepts arguments that control execution. The -v flag prints each command before running it, which helps when debugging a broken pipeline. The -n flag prints the commands without executing them, useful for verifying the plan. You can also pass arguments to the generator tool by appending them after the directive.
//go:generate stringer -type=Status -output=status_string.go
package main
type Status int
Here the stringer tool receives -type=Status and -output=status_string.go as its arguments. The directive parser splits the line on whitespace and passes the tokens to the command.
go generate is a convention, not a language feature. The compiler ignores these comments. If you run go build, the directives never execute. You must run go generate explicitly, or wire it into your build script.
Goroutines are cheap. Generated code is just code.
Minimal example: injecting a timestamp
The simplest generator writes a static value into a file. This pattern is common for embedding build metadata, version strings, or configuration constants. The generator runs at build time and captures the current state.
Create a generator script that writes a Go file. The script uses the os and fmt packages to create the output.
// gen_version.go runs as a standalone program to produce version.go.
package main
import (
"fmt"
"os"
"time"
)
func main() {
// Open the output file for writing, truncating if it exists.
f, err := os.Create("version.go")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create file: %v\n", err)
os.Exit(1)
}
defer f.Close()
// Write the package declaration and the generated variable.
// The timestamp is captured when the generator runs.
fmt.Fprintf(f, "// Code generated by gen_version.go. DO NOT EDIT.\n\n")
fmt.Fprintf(f, "package main\n\n")
fmt.Fprintf(f, "// BuildTime is the time when the code was generated.\n")
fmt.Fprintf(f, "const BuildTime = %q\n", time.Now().Format(time.RFC3339))
}
The generator writes a file with a specific header comment. The comment // Code generated by ... DO NOT EDIT. is a convention recognized by linters and tools. It tells them to skip the file during analysis. Generated code often violates style rules or contains repetitive patterns. The header prevents false positives and keeps the signal-to-noise ratio high in your CI pipeline.
Add the directive to your main file.
//go:generate go run gen_version.go
package main
import "fmt"
func main() {
fmt.Println("Built at:", BuildTime)
}
Run the generator and then the program.
go generate
go run .
# output:
# Built at: 2024-05-20T14:32:00Z
The generator creates version.go. The compiler sees version.go as a normal source file. It links the BuildTime constant into the binary. If you run go generate again, the timestamp updates. The workflow is deterministic and repeatable.
Generated code is just code. It compiles like everything else.
Realistic workflow: generating a mock
A more complex use case involves generating code that implements an interface. This is useful for testing. You define an interface in your library. You write a generator that produces a mock implementation. When the interface changes, you re-run the generator and the mock updates automatically.
Define the interface in greeter.go.
//go:generate go run gen_mock.go
package main
// Greeter handles greeting logic.
type Greeter interface {
Greet(name string) string
Farewell(name string) string
}
The generator script reads the interface definition or uses a template to produce the mock. For this example, the generator writes a hardcoded mock. In production, you would use a tool like mockgen that parses the AST, but the principle is identical.
// gen_mock.go produces a mock implementation of the Greeter interface.
package main
import (
"fmt"
"os"
)
func main() {
// Create the mock file.
f, err := os.Create("mock_greeter.go")
if err != nil {
fmt.Fprintf(os.Stderr, "create error: %v\n", err)
os.Exit(1)
}
defer f.Close()
// Write the mock struct and methods.
// The mock returns fixed strings for testing.
fmt.Fprintln(f, "// Code generated by gen_mock.go. DO NOT EDIT.")
fmt.Fprintln(f, "package main")
fmt.Fprintln(f)
fmt.Fprintln(f, "// MockGreeter implements Greeter for tests.")
fmt.Fprintln(f, "type MockGreeter struct{}")
fmt.Fprintln(f)
fmt.Fprintln(f, "func (m *MockGreeter) Greet(name string) string {")
fmt.Fprintln(f, "\treturn \"Hello, \" + name")
fmt.Fprintln(f, "}")
fmt.Fprintln(f)
fmt.Fprintln(f, "func (m *MockGreeter) Farewell(name string) string {")
fmt.Fprintln(f, "\treturn \"Goodbye, \" + name")
fmt.Fprintln(f, "}")
}
Run the generator.
go generate
The directory now contains mock_greeter.go. You can use the mock in tests.
// main_test.go uses the generated mock.
package main
import "testing"
func TestGreeter(t *testing.T) {
g := &MockGreeter{}
result := g.Greet("Alice")
if result != "Hello, Alice" {
t.Errorf("got %q, want %q", result, "Hello, Alice")
}
}
If you add a method to Greeter, the mock will no longer compile. The compiler error tells you exactly what is missing. You update the generator to include the new method, run go generate, and the mock compiles again. The generator keeps the mock in sync with the interface.
The worst goroutine bug is the one that never logs. The worst generator bug is the one that produces stale code.
Pitfalls and conventions
Generators introduce a new failure mode. The generated file might not exist, or it might be out of date. The compiler cannot detect this automatically. If you forget to run go generate, the build fails with an undefined identifier error.
./main.go:10:2: undefined: BuildTime
This error looks like a missing import or a typo. It is actually a missing generation step. The fix is to run go generate before building. Many teams add a wrapper script or a Makefile target that runs go generate ./... followed by go build. The ./... pattern tells the tool to scan all packages in the current directory tree. Without it, go generate only runs in the current directory.
Generators must be idempotent. Running the generator twice should produce the same result. If your generator appends to a file instead of overwriting it, the file grows every time you run the tool. This breaks reproducibility. Always open output files with os.Create or os.OpenFile with the os.O_TRUNC flag to ensure a clean slate.
Path resolution is a common source of confusion. The command runs in the directory of the source file. If your directive uses a relative path, it is relative to that directory.
//go:generate ../tools/gen.sh
package main
This command runs gen.sh from the parent directory. If you move the file, the path breaks. Use absolute paths or rely on go run with package paths to avoid fragility.
The go generate tool does not manage dependencies. It does not know if the generator tool is installed. If the tool is missing, the command fails with an executable error.
go generate: main.go:1: running "mockgen": exec: "mockgen": executable file not found in $PATH
Install the tool using go install or use go run to execute it directly. The go run approach is safer for tutorials and small projects because it fetches the tool automatically. For large projects, installing the tool once is faster.
Convention dictates that generated files should not be edited by hand. If you need to customize the output, change the generator. Hand-editing generated code leads to divergence. The next time you run the generator, your changes disappear. Treat generated files as immutable artifacts.
Trust the generator. If the output is wrong, fix the tool, not the file.
When to use go generate
Code generation is powerful, but it adds complexity. You trade manual effort for automation. The automation must be worth the cost. Use the decision matrix below to choose the right approach.
Use go generate when you have a deterministic transformation from source to source and the output is Go code. Use go generate when the generation step is fast and can run locally without external services. Use go generate when you want the build dependencies to be visible in the source file via directives.
Use a build tag when you need to include or exclude code based on the operating system, architecture, or custom tags. Build tags are evaluated by the compiler and do not require a separate generation step.
Use a Makefile or shell script when the generation involves complex environment setup, database migrations, or non-Go artifacts like Docker images. go generate is designed for source-to-source transformations. It is not a general-purpose build system.
Use runtime configuration when the data changes per deployment and does not belong in the binary. Generating code for configuration that changes frequently defeats the purpose of compilation. Load configuration from files or environment variables at runtime instead.
Use runtime reflection when the structure is dynamic and known only at runtime. Reflection is slower than generated code, but it handles variability that generation cannot predict.
Use go generate when the output is deterministic Go code derived from source or assets. Use a build tag when you need conditional compilation based on environment. Use a Makefile when the generation depends on non-Go assets or complex shell logic. Use runtime reflection when the structure is dynamic and known only at runtime.
Go tools are simple. Keep your generators simple.