skip to content

What does a `//go:generate` line above an interface do, and when does that command actually run?

level: juniorimportance: should knowfreq 48%

answer

  1. it is only a comment
  2. one subcommand knows about it
  3. the build never triggers it
  4. generated files are committed like source
  5. adding a method breaks compilation

basics

~20 s

It is an ordinary comment that the compiler ignores. Only an explicit go generate run scans source files for lines starting with //go:generate and executes the rest as a command in that package's directory. go build and go test never trigger it.

solid answer

~40 s

A `//go:generate` line is a plain comment with a magic prefix: nothing in the normal build pipeline reads it. Running `go generate ./...` walks the named packages, finds every line that begins exactly with `//go:generate` (no space between the slashes and `go:`), and runs the remainder as a shell-less command with the package directory as the working directory, exporting `$GOFILE`, `$GOPACKAGE` and `$GOLINE`. For mocks that means the generated file is produced by a human running the command, then checked into the repository like any other source. `go build` and `go test` compile whatever mock file is on disk, stale or not. The safety net is the type system: if the interface gains a method and nobody regenerates, the mock stops satisfying it and the test package fails to compile.

code

go · 6 lines
go
//go:generate go run ./internal/gen/mocks -type PlatformClient -out platform_mock_test.go

type PlatformClient interface {
	Get(ctx context.Context, name string) (Spec, error)
	Update(ctx context.Context, s Spec) error
}

go deeper

for a junior

Recall the two facts that matter: the line is a comment, and only go generate runs it. Be able to say that the resulting file is committed like ordinary source.

for a middle

Explain the mechanics — the exact prefix with no space, the package directory as working directory, $GOFILE and $GOPACKAGE, and that there is no shell, so pipes and globs do not work.

for a senior

Show how you keep generated files honest in a repository: a CI step that regenerates and diffs, and a compile-time interface assertion so a stale mock fails the build at an obvious place.

for a principal

Be ready to argue why Go keeps generation out of the build at all — reproducible builds and reviewable diffs — and what that pushes onto your CI and onboarding instead.

## What the directive is `//go:generate` is not a language feature. It is a comment convention that one subcommand of the Go toolchain, `go generate`, knows how to find. The compiler, `go build`, `go test`, `go vet` and your editor all treat it as a comment and move on. The syntax rules are stricter than they look: - The line must **begin** with `//go:generate` — no space between `//` and `go:`, and the directive must be the first thing on the line. - Everything after it is the command and its arguments, split on spaces (quoting is supported, but there is **no shell**: no pipes, no `&&`, no globbing). - Placement in the file is free. By convention it sits directly above the interface it generates a mock for, so a reader of the interface can see that a double exists. ```go //go:generate go run ./internal/gen/mocks -type PlatformClient -out platform_mock_test.go type PlatformClient interface { Get(ctx context.Context, name string) (Spec, error) Update(ctx context.Context, s Spec) error } ``` ## When it runs Only when a person or a CI step runs `go generate`, typically `go generate ./...` from the module root. The go command then, for each matching package, executes each directive in file order with: - the **package directory** as the working directory, and - environment variables describing the site: `$GOFILE` (the file containing the directive), `$GOPACKAGE` (the package name), `$GOLINE` (the line number), plus `$GOOS`/`$GOARCH` and `$DOLLAR`. Useful flags: `-n` prints the commands without running them, `-x` prints them as it runs them, and `-run` takes a regular expression matched against the directive text so you can execute only some of them. The critical negative: **generation is not part of the build**. Go deliberately has no implicit code-generation step, so a `go build` on a clean checkout compiles exactly the bytes in the repository. Generated mocks are therefore normal source files that are committed. That is why teams add a CI step that runs `go generate ./...` and fails if the working tree changed — without it, nothing tells you the checked-in mock is out of date. ## What happens when a mock goes stale This is the part worth being precise about in an interview, because it is Go's type system doing the work rather than a runtime check: - **A method is added to the interface.** The generated mock no longer implements it. Anywhere the mock is passed where the interface is expected, compilation fails with `missing method`. This is the good case: you find out at build time. - **A method's signature changes.** Same outcome — the mock's method no longer matches, so assignment to the interface fails. - **A method is removed from the interface.** The mock still satisfies the interface (it just has a surplus method), so everything compiles and the stale mock silently lives on. This case does not self-report. A cheap way to make the first two failures point at the mock instead of at a distant call site is a compile-time assertion in the generated or test file: ```go var _ PlatformClient = (*mockPlatformClient)(nil) ``` That declaration costs nothing at runtime — the value is discarded — but it fails to compile the moment the type stops implementing the interface, with an error naming the mock. ## Practical consequences - The directive is documentation as much as automation: it records exactly which command produced the file, so the next person regenerates it the same way. - Because there is no shell, `//go:generate go run ./internal/gen/mocks ...` (invoking a program by package path) is more portable than relying on a binary being on `$PATH` at some particular version. - The generated file is reviewable source. Read it once; it is usually mechanical and dull, which is the point. - Nothing about `//go:generate` is specific to mocks. The same mechanism produces string methods for enums, protocol bindings, embedded assets and lookup tables — mocks are just the most common testing use.

  • If nobody reruns the command after an interface gains a method, when do you find out?
    At compile time, not at test time. The generated mock no longer implements the interface, so every place it is passed as that interface fails with a missing-method error. The reverse case is silent: removing a method from the interface leaves the mock with a surplus method, which still satisfies the interface and compiles fine.
  • How would you stop a stale generated file from reaching the main branch?
    Add a CI step that runs `go generate ./...` and then fails if the working tree is dirty — for example `git diff --exit-code`. That proves the committed files match the current interfaces. It requires CI to have the generator available at the pinned version, which is part of the cost of choosing generation.
  • Why does the go command not run these directives during `go build`?
    By design: a build compiles exactly the source in the repository, with no hidden step that can rewrite files, need network access, or produce different output on different machines. Keeping generation an explicit, human-invoked action makes builds reproducible and keeps generated code reviewable in the diff.

saying these in an interview costs you the question

  • Thinks `go build` or `go test` runs //go:generate lines
  • Writes `// go:generate` with a space, so it is never found
  • Assumes generated mocks need not be committed
  • Expects a stale mock to fail at runtime rather than at compile time
  • Believes the directive line is executed by a shell, with pipes