JavaRush /Courses /Go SELF /go generate: benefits, transparency

go generate: benefits, transparency

Go SELF
Level 36, Lesson 4
Available

1. Introduction

If you have ever written the same thing twice, you are already on the road to code generation. It usually starts innocently: “I’ll just quickly write String() with a switch,” then a second enum appears, then a third, then you rename a value, forget to update the string, and now you have a bug that is very hard to find because it looks like “just the wrong text.” At that moment you start to realize that manual boilerplate is not heroism—it is technical debt disguised as productivity.

go generate is a mechanism that lets you explicitly run code generators tied to source files via special comments. That is, generation stops being a secret ritual of “it works on Pete’s laptop,” and becomes a reproducible step that you can see right in the repository.

And here is the key thought of the day: generation is not “magic,” it is just automation of boring repetition. If generation starts changing the meaning of the program depending on the moon phase and the current time, that is no longer generation—it is a horror plot.

2. How go generate works

It is important to understand the mechanics without mysticism: go generate scans Go sources and looks for comment lines in a special format. For each found directive, it runs the specified command. At the same time, go generate is not part of go build; it will not “figure things out on its own,” and it does not do dependency analysis. You must run it explicitly when you decide to update the generated files.

This separates the world into two clear phases: “I changed the input data” and “I ran generation and got an updated result.” This approach is chosen in Go on purpose: to keep builds predictable and fast, and generation intentional.

To make the picture completely clear, imagine a typical workflow as a diagram:

flowchart TD
    A[Edit sources: enum, tags, schemas] --> B[Run go generate]
    B --> C[*_gen.go / *_string.go updated]
    C --> D[go test]
    D --> E[Commit: sources + generated files]

One more fundamental point: go generate is designed primarily for the package author, not for the user of your library. That is why generated files are usually committed to the repository, so the client does not need to have all the generators installed.

Directive //go:generate

Go’s approach to directives is simple: for a line to be considered a directive, it must start exactly with //go:generate—with no space after //. If you write // go:generate, you will merely leave a comment “for future archaeologists,” and the command will not run. This strict format is part of the “no magic” idea: either the directive is recognized unambiguously, or it is not.

Typically, you place the directive near the place that actually needs generation. Not “in some separate generation file with 300 lines,” but right next to the type, constants, or structures that are the source of truth. Then the reader can see: here is the type, here are the constants, here is the command that generates the support.

A mini example of “what the directive looks like” (not tied to our project yet):

package todo

//go:generate stringer -type=Status
type Status int

The command you specify after //go:generate runs as an external program. Often it is one of three options: a standalone utility (for example, stringer), go run ./internal/... to run your generator, or something like goyacc to generate parsers. The fact that Go does not limit you to a single utility is what makes go generate universal.

You can run generation selectively (in the current package) or recursively across the project. In practice, people usually use one of these commands: go generate (current folder) or go generate ./... (all packages recursively). In this lecture, we keep the second option in mind as “update everything.”

3. Example: Status and generating String() via stringer

Now let’s assemble a piece of our training app (package todo) so it feels realistic: a task has a status, and we want to print that status in a human-friendly way. We could always print numbers, of course… but then your CLI will look like bargain-basement accounting software.

Let’s start with a simple Status based on int and a set of constants via iota:

package todo

// Status describes the lifecycle state of a task.
type Status int

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusDone
)

Now the naive path: write the String() method by hand. It is honest, it works, and at first it is even useful to understand the mechanics:

package todo

import "fmt"

// String returns a human-readable status name.
func (s Status) String() string {
	switch s {
	case StatusOpen:
		return "open"
	case StatusDone:
		return "done"
	default:
		return fmt.Sprintf("Status(%d)", s)
	}
}

The problem here is not that the code is bad. The problem is that it quickly stops being “just once”: you add StatusArchived, rename StatusOpen to StatusActive, change the order—and you must remember that String() needs to be updated in sync. This is where generation is the perfect tool: it is mechanical work that the computer will do more reliably than you.

Hooking up stringer via go generate

Go has a popular utility stringer (from the golang.org/x/tools repository) that generates String() for sets of integer constants. Important: stringer is not part of the standard library, but it is “native in spirit” and widely used.

Let’s add the directive next to the type:

package todo

//go:generate stringer -type=Status

type Status int

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusDone
)

Now, when you run go generate in this package, stringer will create a file (usually with the _string.go suffix) containing the generated String() method.

Checking the effect in a tiny piece of code

To feel the benefit, let’s make a mini snippet that prints the status. This can be a piece of your CLI main or just a temporary experiment:

package main

import (
	"fmt"

	"example.com/todoapp/todo"
)

func main() {
	st := todo.StatusDone
	fmt.Println(st) // StatusDone (after running stringer)
}

Before generation, fmt.Println(st) would print a number (2) if there is no String(). After generation, fmt will see the String() method and output a string. And yes, this is one of those cases where “pretty output” is not cosmetics—it is debugging convenience and UX.

4. Living with generated files

When a generator creates a file, it usually adds a header warning like Code generated ... DO NOT EDIT.. This is not a threat, it is care for your sanity: so you do not spend an hour editing a file that will be regenerated and overwritten a minute later. In the stringer case, this is considered good form: the generated file starts with such a line.

It can look roughly like this (simplified, so we do not drown in stringer implementation details):

// Code generated by stringer -type=Status; DO NOT EDIT.

package todo

func (s Status) String() string {
	// ... generated mapping ...
	return ""
}

Now for the main organizational question: what do you do with this file in git? In Go it is customary to keep generated files next to the sources if they are needed to build or use the package, because the client of your package is not required to be able to run your generators. This makes the project less “fragile”: the build depends on the sources and the already-generated result, not on the presence of tools on the user’s machine.

This leads to the requirement of determinism: if two developers run go generate on the exact same version of the sources, the result must be identical. Otherwise, git diffs turn into noise and the team turns into a club of “who got it to work today.”

In practice, determinism is usually broken by three things: generation that depends on the current time; generation that depends on randomness; generation that depends on the environment (paths, locale, tool versions). That is why “healthy” generation tries to be as pure as possible: same input → same output.

5. How not to make a project “magical”

The most common problem with go generate is not technical, but organizational: you add generation, everything works, and a month later nobody remembers that it needs to be run, with which tool version, and why you even have a file named something_gen.go that must not be touched. To prevent this, you need to follow a few simple rules that sound boring but save the project from “paranormal activity.”

Rule one: generation must be explicit and must not pretend to be part of the build. That is the philosophy of go generate as a command: it is deliberately not built into go build. Then you always have a clear question: “did you run generation?”—and a clear answer: “yes/no.”

Rule two: the generation command must be visible in the code, next to the source of the data. That is exactly why the directive is placed next to Status, not “somewhere in the scripts folder.” When you read the type, you immediately see “aha, stringer is used here, so String() is generated.”

Rule three: generation should serve what is truly mechanical. String() for an enum is a great example: it does not add business meaning, it just saves you from routine and from keeping strings in sync with constants.

Rule four: document how to run generation where people actually look. Typically this is README and/or doc.go. If a package has generation, a short “Development” section in the project README is perfectly appropriate, saying: “after changing statuses, run go generate ./....”

Rule five: keep examples and tests on the side of truth. Example tests are great because they do not let documentation rot: if you show fmt.Println(StatusDone) in an example, then if generation breaks the example will start failing (or at least stop matching the expected output). This is exactly the idea of “documentation you can execute,” which is why examples exist at all.

Here is a small Example you can put into status_example_test.go and thereby “nail down” expectations about generation:

package todo

import "fmt"

func ExampleStatus_String() {
	fmt.Println(StatusDone)
	// Output: StatusDone
}

Yes, by default stringer prints the constant name (StatusDone), not "done". And that is fine: the generator does exactly what it promises. If you need “done/open,” that is a separate display policy (you can do it manually, and keep stringer for technical debugging). What matters is that the example fixes the contract and becomes verifiable.

6. Common mistakes when using go generate

Mistake #1: expecting generation to happen “automatically during go build.”
This is probably the most common trap. After a couple of days with Go, your brain gets used to: “I wrote code—it built.” But go generate is deliberately not built into the build process and must be run separately, with no automatic dependency analysis. If you forget that, you will get the situation “it works for me, but my colleague can’t compile it,” because you happen to have a fresh generated file locally.

Mistake #2: writing // go:generate and being surprised that nothing happens.
The directive is recognized only in the strict format //go:generate. No “well you understood what I meant.” Go here is like a compiler: it does not read minds, it reads characters. It is annoying exactly once, and then you start to love the predictability.

Mistake #3: editing the generated file by hand.
A generated file often starts with “DO NOT EDIT” not out of spite. If you edit it manually and then someone runs go generate, your change will disappear without a trace, as if it never existed. The correct place to change things is either the input data (enum/constants), the generator parameters, or the generator itself.

Mistake #4: making generation non-deterministic.
If generation adds the current date, random numbers, map iteration order, or a dependency on locale, you will get endless diffs and conflicting commits. The whole idea of “a machine generates boring code” assumes repeatability. Otherwise you just swap one kind of pain for another, more exotic one.

Mistake #5: hiding generation and disconnecting it from meaning.
When the directive is far away from the type it is for, a new developer will perceive generation as a sudden natural phenomenon: “there is some _string.go file in the folder, I guess the wind brought it.” It is much better to keep //go:generate next to the data source (in our example, next to Status), so the connection is obvious at a glance.

1
Task
Go SELF, level 36, lesson 4
Locked
Ping Generator
Ping Generator
1
Task
Go SELF, level 36, lesson 4
Locked
Status names
Status names
1
Task
Go SELF, level 36, lesson 4
Locked
Example contract
Example contract
1
Task
Go SELF, level 36, lesson 4
Locked
Order labels
Order labels
1
Survey/quiz
Documentation, 36 level, 4 lesson
Unavailable
Documentation
Documentation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION