go generate is a command that scans your source files for special //go:generate comments and runs whatever shell commands those comments name. It's not a build step, it's not a dependency manager, and it doesn't know anything about the tools it invokes. It exists so that the same workflow handles every code generator your project uses: stringer, mockgen, sqlc, protoc, oapi-codegen, whatever. This chapter covers the directive syntax, the environment it provides, the common generators you'll meet in e-commerce projects, and the workflow questions teams argue about (commit generated code or not, how to pin tool versions, when to use a Makefile instead).
Generated code is everywhere in real Go projects. An OrderStatus enum gets a String() method from stringer. A PaymentGateway interface gets a mock from mockgen. SQL queries become typed Go functions through sqlc. Protobuf definitions become Go structs through protoc-gen-go. OpenAPI specs become HTTP handlers through oapi-codegen.
Before go generate existed, every project invented its own conventions. Some teams put a Makefile with make gen targets. Others wrote shell scripts in a scripts/ directory. A few added go:build ignore files that generated code when run with go run. The result was that every codebase had a different ritual for "regenerate the boring code", and onboarding a new engineer meant learning yet another set of project-specific commands.
go generate is the Go team's answer: standardize the trigger, leave the generators alone. The trigger is always go generate./.... The generators are whatever you wire up. Nothing is built, nothing is downloaded, nothing is checked. The command simply finds //go:generate comments in your source files and runs the commands those comments contain. This is the whole feature.
When you run go generate./..., the tool walks every package in the module, opens every .go file, looks for lines starting with //go:generate, and executes each one. The command runs in the directory of the file it appeared in, with environment variables that tell it which file and package triggered it. If the command fails, go generate stops and returns a non-zero exit code.
Two things this command does not do are worth saying out loud. It does not run on go build. It does not run on go test. Generated files have to exist before the build, which means somebody, either a human or a CI step, must invoke go generate explicitly. The Go team made this choice deliberately, so that builds stay fast and reproducible. We'll come back to the trade-off later when we talk about committing generated code.
The directive is a magic comment. The //go:generate prefix has to start at column 1 of the line (no leading spaces or tabs), and there must be no space between // and go:generate. Anything else and the comment is invisible to the tool.
That's the entire wiring. Save the file, run go generate./..., and stringer produces a status_string.go file next to it with a String() method for Status.
A few placement rules matter. The directive can sit anywhere in the file (top, middle, just above the type it generates code for), but most teams put it directly above the type or interface it relates to so the connection is obvious. You can have multiple directives in one file, and they run in the order they appear. You can also have directives in different files in the same package, and go generate runs all of them, file by file in alphabetical order.
The command after //go:generate is run through your shell's argument parsing rules, but it's not actually passed to a shell. So you can split arguments with spaces and quote strings that contain spaces, but pipes, redirects, and shell expansions do not work. If you need a pipeline, wrap it in a real script and have the directive call the script.
That works because you explicitly invoked sh. Without sh -c, the && is just an argument to protoc, which would confuse it.
When go generate runs your command, it sets four environment variables that describe the context. Generators use them to know which file or package they're working on.
| Variable | Value | Used for |
|---|---|---|
$GOFILE | The name of the source file (e.g., order.go) | Telling the generator which file triggered it |
$GOPACKAGE | The package name from the source file | Putting that name at the top of the generated file |
$GOLINE | The line number of the directive | Diagnostics in error messages |
$DOLLAR | A literal $ sign | Embedding a $ in the command without shell escaping |
These work on every operating system because go generate expands them itself before invoking the command. You don't need shell substitution.
Run go generate./report and is:
That's a toy example, but it shows what the variables look like to a real generator. stringer uses $GOFILE to figure out where to write its output. mockgen uses $GOPACKAGE to set the package declaration in the mock file. Most generators don't need these directly because their own flags supply the same information more explicitly, but they're there when you write your own.
$DOLLAR exists because Go's directive parser strips $VARIABLE references and replaces them with the variable's value. If you need a literal dollar sign in the command (regex, currency, shell variable for a downstream script), $DOLLAR is how you smuggle it through.
Forgetting that $GOFILE is just a filename, not a full path, is a common pitfall when writing custom generators. The command runs in the directory of the source file, so $GOFILE is relative to that directory.
stringer is the canonical Go code generator. It turns a set of integer constants (used as an enum) into a type that implements fmt.Stringer, so printing a value gives you "StatusPaid" instead of 1.
Without stringer, you'd write the method by hand:
This works, and for five values it's fine. But the next person who adds StatusRefunded has to remember to update the switch, and if they don't, fmt.Println(StatusRefunded) prints Status(5) instead of "Refunded". That kind of silent drift is exactly what generators prevent.
Here's the same enum with stringer:
Install the tool once with go install golang.org/x/tools/cmd/stringer@latest, then run go generate./... from the module root. A file called status_string.go appears next to order.go:
The DO NOT EDIT header is conventional, and tools like go vet and CI lint steps recognize it. The compile-time check at the top is stringer's way of catching drift: if you reorder the constants or remove one, the indices stop matching and the file fails to compile, which is your signal to regenerate.
Now if you add StatusRefunded to the const block and forget to regenerate, the build breaks with a clear error pointing at status_string.go. That's the value of the pattern: the generated file becomes a contract the compiler enforces.
stringer writes a constant string _Status_name and an index table. For an enum with five values that's a few hundred bytes. For one with thousands of values it's still cheap, much smaller than a switch with thousands of cases would compile to.
mockgen (from go.uber.org/mock) generates mock implementations of interfaces for use in tests. In an e-commerce service, the order placement code probably depends on a payment interface. The real implementation talks to a payment provider over the network. Tests should not do that, so you need a mock.
Here's the interface:
Hand-writing a mock for this is mechanical. You'd build a struct that records calls, lets the test set return values, and implements every method. For a two-method interface it's twenty lines of tedious code. For an interface with fifteen methods it's an afternoon and a few subtle bugs.
mockgen automates it:
After installing mockgen with go install go.uber.org/mock/mockgen@latest and running go generate./..., you get a mock_gateway.go file containing a MockGateway struct with all the wiring tests need. The generated file is long, but you never read or modify it. You use it from tests:
Two flags do the work in the directive. -source=gateway.go tells mockgen which file to read interfaces from. -destination=mock_gateway.go tells it where to write the result. -package=payment tells it to put the mock in the same package as the production code, which lets tests use it without an extra import.
A common alternative is reflection mode, which generates mocks by inspecting compiled types rather than parsing source files. That's useful when the interface lives in a third-party package you can't annotate:
The trade-off is that reflection mode requires the package to compile cleanly during generation, while source mode only needs the source file to parse. Most internal interfaces use source mode because it's faster and produces clearer errors.
Mocks balloon test binaries. A package with twenty interfaces and twenty mocks adds noticeable kilobytes to every test binary that imports them. For very large codebases, keeping mocks in a separate mocks/ package (only imported by tests) keeps the impact contained.
The mechanics are simple enough to describe in full. When you invoke go generate./..., the tool does this:
The walk order is deterministic: packages in import-graph order, files in alphabetical order within each package, directives in source order within each file. That matters when one generator's output feeds another's input. If protoc needs to run before mockgen (because mockgen mocks an interface from a .pb.go file), put the protoc directive in a file that sorts earlier.
You can scope a run with package patterns like go test:
There's also a -run flag that filters by a regex matched against the command. So go generate -run=stringer./... runs only the directives whose command starts with stringer, which is useful when one generator is slow and you only need to refresh another.
The -x flag echoes each command before running it, which is the first thing to use when a directive does something unexpected:
And -n does a dry run: print what would run, do nothing. Handy when you've added a directive and want to confirm it's seen before actually firing it.
Generators are themselves Go programs. stringer lives at golang.org/x/tools/cmd/stringer. mockgen lives at go.uber.org/mock/mockgen. When you run go install on one of them, the version you get depends on what's in your GOPATH or the version you typed after @. That's bad for reproducibility. Two engineers running go generate should get byte-identical output, regardless of when they last installed the tools.
The fix is the tools.go pattern. You create a file in your module whose only purpose is to import the generator packages with a build tag that prevents normal builds from compiling it.
A few things are happening here. The //go:build tools tag means this file is only compiled when you pass -tags=tools, which go build and go test never do by default. The blank imports _ "..." mean "I'm not using this package, but I want it in my go.mod". And the package tools declaration keeps the file in its own namespace so it doesn't pollute your real packages.
Once this file exists, go mod tidy adds entries for stringer and mockgen (and their dependencies) to your go.mod. The module is now version-locked to the tool versions you chose. Anyone cloning the repo can install the exact same versions with:
Without @latest or @vX.Y.Z. Because the module's go.mod already pins those packages, go install picks them up at the pinned version. This is the whole trick: tools.go is a way to write down "these generators are part of the project" in a place that Go's module system understands.
Some teams formalize the install step in a Makefile:
That makes a fresh checkout into a single make generate. New engineers don't need to remember which tools exist.
Go 1.24 added an alternative: the tool directive in go.mod, which records tool dependencies more directly than the tools.go workaround. If your module is on Go 1.24 or later, you can replace the tools.go file with this:
And run tools with go tool mockgen... or go tool stringer.... The tools.go pattern still works and is what most existing codebases use, so it's both.
The tools.go file means go mod tidy will not prune the generator dependencies from your go.mod. That keeps the module file slightly larger than it would otherwise be, which is the price of reproducibility.
stringer and mockgen are the two most common, but the broader toolchain has more. Each follows the same pattern: a CLI tool you install, a //go:generate directive that invokes it, an output file you check in (or don't, depending on your workflow).
`sqlc` turns SQL queries into typed Go functions. You write INSERT INTO orders... in a .sql file, and sqlc reads your schema, generates a struct for the orders table, and a Go function that runs that exact query with typed parameters.
Most teams put the directive in a top-level db/ directory and let sqlc.yaml carry the configuration. The output is a db/queries.sql.go file with one Go function per named query.
`protoc-gen-go` turns Protocol Buffers definitions into Go structs and gRPC stubs. The directive usually invokes protoc (the protobuf compiler) with a Go plugin:
The output is a order.pb.go file containing message structs and a order_grpc.pb.go file containing client and server stubs. For services that talk to other services over gRPC, this is the standard wiring.
`oapi-codegen` does the same thing for OpenAPI (Swagger) specs. You write openapi.yaml describing your HTTP endpoints, and the tool generates request/response types, a server interface, and (optionally) a client.
The point of listing these is not to teach each one in depth, those are full chapters elsewhere, but to show that go generate is a single front door for very different generators. Each one has its own configuration, its own templates, its own output style. go generate doesn't care. It runs them.
| Generator | Input | Output | Typical command |
|---|---|---|---|
stringer | Go const block | String() method | stringer -type=Status |
mockgen | Go interface | Mock struct for tests | mockgen -source=foo.go -destination=mock_foo.go |
sqlc | SQL files + schema | Typed Go query functions | sqlc generate |
protoc-gen-go | .proto files | Message structs, gRPC stubs | protoc --go_out=. file.proto |
oapi-codegen | OpenAPI YAML | HTTP server/client code | oapi-codegen -package=api spec.yaml |
That table covers most of what you'll encounter in a Go e-commerce backend. Each tool has flags, configuration files, and gotchas worth their own deep dive, but the integration with go generate is identical: a one-line directive in a Go source file.
This is the question teams argue about. The two camps:
Commit generated code. Run go generate./... locally, check the resulting files into git, push. Pull requests show the diff in generated files alongside the source change, which makes reviews concrete: "you changed the interface, here's what the mock looks like now". CI doesn't need the generators installed. A fresh git clone is a buildable project.
Regenerate in CI. Don't commit generated files. Add them to .gitignore. CI runs go generate./... as a build step. Pull requests are smaller because they don't carry generated diffs. The repo is smaller. No risk of "I forgot to regenerate".
Both work. Most Go projects pick the first one, but neither is wrong.
| Aspect | Commit generated code | Regenerate in CI |
|---|---|---|
| Repo size | Larger | Smaller |
| Build determinism | Strong (source + git is enough) | Depends on CI's tool versions |
| PR diff size | Larger (mock files appear) | Smaller |
| New engineer onboarding | git clone && go build works | Must install tools first |
| Drift between source and generated | Visible in git diff | Only visible in CI failures |
| CI complexity | Lower | Higher (must install generators) |
The pragmatic recommendation: commit generated code unless the files are massive (think megabytes of protobuf or sqlc output). Even then, consider a hybrid: commit the small stuff (mocks, stringers) and regenerate the large stuff in CI.
A useful CI check regardless of your choice is to run go generate./... and then git diff --exit-code. If anything changed, the build fails. That catches the "I edited the source but forgot to regenerate" mistake before it gets merged.
That single check is what makes the "commit generated code" workflow safe. Without it, generated files can drift from their sources, and nobody notices until something breaks at runtime.
A reasonable question is: why use go generate at all? You could put commands in a Makefile. Or write a gen.sh shell script. Or invoke go run on each generator with the right flags. All three work.
The argument for go generate is that the directives live next to the code they generate. A reader who opens order.go sees the //go:generate stringer -type=Status line and knows immediately how status_string.go got there. Compare that to a Makefile rule fifty lines into a file in the repo root: same information, harder to find.
The argument against is that go generate is one global command with no targets and no dependencies. It doesn't know that regenerating mocks is fast and regenerating protobuf is slow. It doesn't know that the protobuf output hasn't changed since yesterday. A Makefile can express both of those things naturally with file timestamps and target rules.
In practice, most teams use both. Makefile for the orchestration (make generate calls go generate./... plus any non-Go generators like Tailwind), and //go:generate directives for the Go-specific work. The Make target is the front door; go generate is the worker behind it.
go run on each generator directly is the simplest of the three, but it doesn't scale beyond two or three generators. You end up with a shell script anyway, which is what Makefile and go generate were trying to avoid.
The honest answer is that they're not exclusive. Use go generate for in-source directives. Use a Makefile (or taskfile, or your CI's equivalent) to chain go generate with non-Go steps. Don't pick one and ban the others.
Generators solve repetition, but they introduce their own complexity: a new tool to install, a new file to read, a new step in the pipeline. They're worth it when the alternative is genuinely worse. They're not worth it when the alternative is fine.
A few situations where a generator is overkill:
stringer for a five-value enum that changes once a year doesn't justify the tooling overhead, though most teams add it anyway because the marginal cost is small.PaymentGateway is implemented as a type FakeGateway struct{} in your test file, you don't need mockgen. Mocks earn their keep when the interface has many methods, complex argument matching, or call-order assertions.gen.sh. Don't add a //go:generate directive for something you'll run once.The rule of thumb most Go teams converge on: use generators when the generated code is large or repetitive (mocks, protobuf, sqlc), and write it by hand when the generated code would be smaller than the directive that produced it.
10 quizzes