Popular Code Generation Tools in the Go Ecosystem

The Go ecosystem relies heavily on `go generate` as its standard mechanism for triggering code generation, typically powered by tools like `stringer`, `mockery`, and `golangci-lint`'s `gofmt` integration.

When constants print as numbers

You define a set of status codes for a job processor. You use iota to keep the values tidy. You print a status to the log and see 0. You want Pending. In Python, the enum handles the string representation automatically. In JavaScript, you might map the value in a lookup table. In Go, the compiler gives you exactly what you asked for: an integer.

You could write a String() method by hand. That works for one type. When you have five enums across three packages, the repetition becomes noise. You look for a macro or a template engine. Go doesn't have macros. Go has go generate. It's a built-in command that runs tools to write Go files. The generated files live in your repository alongside your hand-written code. The compiler treats them identically.

How code generation fits the build

Code generation in Go is explicit and separate from compilation. The go build command never runs generators. It only compiles files that already exist on disk. You run go generate to create files, then you run go build to compile them.

Think of go generate as a scribe sitting at your desk. You give the scribe a directive, and the scribe writes a file. You then hand that file to the compiler. The compiler never talks to the scribe. This separation keeps the build fast and reproducible. Every developer gets the same generated code because the files are committed to version control. The generator is a tool, not a runtime dependency.

The mechanism relies on comments. You place a //go:generate comment in a source file. The comment contains a shell command. When you run go generate ./..., the tool scans the package for these comments and executes them. The command runs in the directory of the source file. Standard output is ignored. The tool must write files directly to the filesystem.

Minimal example: stringer

Here's the simplest case: turning integer constants into readable strings without writing the switch statement yourself.

package main

import "fmt"

// Status tracks the lifecycle of a background job.
type Status int

const (
	Pending Status = iota // Zero value for the first constant
	Running               // Iota increments to one
	Failed                // Iota increments to two
)

//go:generate stringer -type=Status // Directive to generate String() method

func main() {
	fmt.Println(Pending.String()) // Calls the generated method
}

The //go:generate comment tells go generate to run the stringer command with the flag -type=Status. The stringer tool is part of the golang.org/x/tools repository. It inspects the Status type, finds the constants, and writes a file named status_string.go. That file contains a String() method that returns the constant names.

Run go generate ./... in your terminal. The tool creates the file. Run go run . and you see Pending. The generated code uses a simple array or switch statement. It has zero reflection overhead. It runs as fast as hand-written code.

Gofmt is mandatory. The stringer tool runs gofmt on its output before writing the file. Your editor runs gofmt on save. The generated code matches your project's formatting style automatically. You don't argue about indentation in generated files.

Realistic example: mocking interfaces

Interfaces are the backbone of Go testing. Writing mock implementations by hand is tedious and error-prone. The mockery tool generates mock structs that implement your interfaces. It records calls and verifies expectations.

Here's a repository interface for a user service.

package repository

import "context"

// UserRepo defines database operations for users.
type UserRepo interface {
	// GetByID fetches a user by their unique identifier.
	GetByID(ctx context.Context, id int) (User, error)
}

// User represents a user record.
type User struct {
	ID   int
	Name string
}

Context is plumbing. Run it through every long-lived call site. The interface takes context.Context as the first parameter. This is a Go convention. The mock generator respects this convention. The generated mock will expect a context argument.

Install mockery and generate the mock.

# Install mockery tool
go install github.com/vektra/mockery/v2@latest

# Generate mock for UserRepo in current package
mockery --dir=. --name=UserRepo --output=./mocks

The command scans the directory for the UserRepo interface. It generates a file named mock_UserRepo.go in the mocks directory. The file contains a struct that implements UserRepo. Each method records the arguments and returns a configurable value. You use the mock in tests to isolate the code under test.

If you forget to run the generator, the compiler rejects the program with undefined: repository.UserRepo if you try to import the mock package, or undefined: NewMockUserRepo if you try to use the constructor. The error is plain text. It tells you exactly what is missing.

Protocol Buffers and gRPC

Protocol Buffers define a contract for data exchange. The protoc compiler reads .proto files and generates code in multiple languages. Go uses plugins to generate Go code. The protoc-gen-go plugin generates structs and serialization methods. The protoc-gen-go-grpc plugin generates client and server stubs for gRPC services.

Here's a generate directive for a proto file.

package main

//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api.proto

The --go_out flag tells protoc to generate Go code in the current directory. The --go_opt=paths=source_relative flag ensures the output path matches the source path. Without this flag, protoc uses import paths to determine the output directory, which often breaks in monorepos. The --go-grpc_out flag generates the gRPC stubs.

The compiler rejects the program with protoc-gen-go: program not found or is not executable if the plugin is missing from your PATH. You need to install the plugins using go install google.golang.org/protobuf/cmd/protoc-gen-go@latest and go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest.

Protobuf generation is verbose by design. The generated code includes validation, serialization, and reflection support. The boilerplate makes the protocol explicit. You trade file size for type safety and performance.

Managing tool dependencies

Generators are tools. Tools have versions. You need to track tool dependencies alongside your code dependencies. The tools.go pattern keeps tools in your go.mod file.

Create a file named tools.go in a tools package.

//go:build tools

package tools

import (
	_ "github.com/vektra/mockery/v2"
	_ "golang.org/x/tools/cmd/stringer"
)

The //go:build tools tag excludes the file from normal builds. The blank imports _ tell the module system to track the dependencies without using them. When you run go mod tidy, the tools stay in go.mod. When you run go generate, the tools are available.

The underscore discards a value intentionally. import _ "pkg" says "I want the side effect of importing this package, which is registering the dependency." Use it sparingly with errors, but use it freely for tool tracking.

Pitfalls and errors

Code generation introduces a new class of bugs. The generated code might be out of sync with the source. The generator might fail silently. The tool might not be installed.

If you forget to run go generate, the compiler complains with undefined: Status.String because the file doesn't exist yet. This error happens at compile time, not runtime. It's easy to fix. Run the generator.

If you modify the source but forget to regenerate, the generated code becomes stale. The String() method might return the wrong name. The mock might miss a new interface method. The compiler catches missing methods with does not implement interface errors. It catches wrong types with cannot use x as y in assignment errors.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. This rule applies to generated code too. If your generator spawns goroutines, ensure they terminate. If your generated code spawns goroutines, ensure they terminate.

The worst goroutine bug is the one that never logs. Generated code should follow the same logging and error handling conventions as hand-written code. If the generator produces code that swallows errors, fix the generator.

go generate is not part of go build. This is the most common confusion. Newcomers run go build and wonder why the code is missing. They run go test and wonder why the mocks are absent. go generate must run explicitly. Add it to your CI pipeline. Add it to your Makefile. Remind your team.

Decision matrix

Use stringer when you have a type with iota constants and need automatic string representations.

Use mockery when you need to generate mock implementations for interfaces to isolate unit tests.

Use protoc with protoc-gen-go when you are defining a contract for data exchange or RPC services.

Use go generate comments when the tool needs to inspect the source file to find types or comments.

Use a Makefile when the generation requires environment setup or chaining multiple commands.

Use plain code when the generated output is trivial and reading the generator adds more cognitive load than reading the result.

Generated code is code. Commit it. Review it. Don't hide behind the generator.

Where to go next