go generate runs a command you name¶
Level: 301 · anyone who has typed a String() method for an enumeration by hand
One line: A //go:generate command args comment names a command; go generate ./... runs it in the file's directory with $GOFILE, $GOPACKAGE and $GOLINE set, and no other step of the toolchain ever runs it — go build compiles what is on disk, so a generated file that is missing is an undefined error and one that is stale is a wrong program — which is why generated files are committed and carry a first line that Go tools recognise.
The module below has an order Status type with four constants and a main that prints each with its String() method. That method is not in the source: status.go carries a //go:generate go run ./gen line, and gen/main.go is a forty-line generator that writes status_string.go — the job that stringer, from golang.org/x/tools, does for real. The script builds first, generates, shows the file, and builds again.
Verified output of go_generate_runs_a_command_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ gofmt -l .
$ go build -o orders .
# example
./main.go:7:25: s.String undefined (type Status has no field or method String)
exit status 1
$ go generate -x ./...
go run ./gen
exit status 0
$ cat status_string.go
// Code generated by gen from status.go:3; DO NOT EDIT.
package main
import "fmt"
func (s Status) String() string {
switch s {
case Pending:
return "Pending"
case Paid:
return "Paid"
case Shipped:
return "Shipped"
case Cancelled:
return "Cancelled"
}
return fmt.Sprintf("Status(%d)", int(s))
}
$ gofmt -l status_string.go
$ go build -o orders . && ./orders
0 Pending
1 Paid
2 Shipped
3 Cancelled
4 Status(4)
Reading the output¶
Building before generating is an ordinary compile error. s.String undefined (type Status has no field or method String): the compiler saw no String method because no file declared one. go generate runs only when it is run — the documentation ↗ says it must be run explicitly, and a go build, go test or go install never runs it for you. A checkout that forgets the step does not build, which is the good case; a checkout whose generated file is stale builds and is wrong.
go generate -x ./... ran go run ./gen, in the package directory. -x prints each command before running it, -n prints without running. The command here is a Go program in the same module, so nothing has to be installed first; a tool from outside the module can be run the same way, or, since Go 1.24, named by a tool directive in go.mod and run as go tool stringer (Go 1.24 release notes ↗).
The first line is a promise to other tools. // Code generated by gen from status.go:3; DO NOT EDIT. matches the regular expression that go help generate ↗ gives, ^// Code generated .* DO NOT EDIT\.$: the line must start the file, say Code generated, end with DO NOT EDIT., and appear before the package clause. The convention was settled in issue 13560 ↗ so that editors, linters and code review could treat the file as machine output — which tools honour it is their documentation's claim, not this page's. What the go command itself does with the line is nothing: it is for people and for tools that choose to read it. The kata below checks a header against the expression.
The generator wrote gofmt-clean Go. gofmt -l status_string.go listed nothing because gen wrote tabs and blank lines where gofmt wants them. A generator that cannot be sure should run its output through go/format.Source — the kata does — or add a second directive, //go:generate gofmt -w status_string.go, which runs after the first.
4 Status(4) is the fallback for a value with no name; stringer generates the same kind of line.
What the command is given¶
Each directive runs with variables that name its own position, and the run has flags to look before it leaps:
Verified output of go_generate_env_and_run_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go generate -n ./...
echo file=invoices.go line=3
echo file=orders.go package=orders line=3 literal=$
echo from tagged.go, which go build never compiles
$ go generate ./...
file=invoices.go line=3
file=orders.go package=orders line=3 literal=$
from tagged.go, which go build never compiles
$ go generate -run literal ./...
file=orders.go package=orders line=3 literal=$
$ go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
[invoices.go orders.go] ignored=[skipped.go tagged.go]
$ go generate ./...
failing.go:3: running "false": exit status 1
exit status 1
$GOFILE, $GOPACKAGE, $GOLINE, $DOLLAR. The file, the package name and the line of the directive, which is how a generator writes from status.go:3 into its header; $DOLLAR is the only way to put a literal $ into the command, because $NAME is expanded by go generate before the command runs, and no shell is involved unless the command is one. -n printed the three commands without running them; -run literal chose the one whose text matched.
A directive starts at column 0 with no space after //. // go:generate in invoices.go was never printed — it is a comment, as // go:build is on the vet page.
Build constraints apply, and the tag generate is set. skipped.go is behind //go:build never and its directive did not run; tagged.go is behind //go:build generate, a tag that only go generate satisfies, so its directive ran and go list shows the file ignored by the build. That is the way to keep a generator's own directives in a file the build never compiles.
A failing command stops the package. failing.go:3: running "false": exit status 1, and go generate exits 1 without running the directives after it — the documentation's rule that an error skips all further processing for that package.
go_generate_runs_a_command_sh.sh and go_generate_env_and_run_sh.sh
go_generate_runs_a_command_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# A //go:generate directive names a command, and go generate runs it --
# nothing else does, go build included. Here the command is a small Go
# program that writes a String method for an order status type, the job the
# stringer tool does outside the standard library. Building before generating
# fails; generating and then building works; the generated file carries the
# header that Go tools recognise.
set -u
export GOTOOLCHAIN=local
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
cd "$dir" || exit 1
say() { printf '$ %s\n' "$*"; }
go mod init example >/dev/null 2>&1 || exit 1
cat >status.go <<'GO'
package main
//go:generate go run ./gen
// Status is the state of an order. Its String method is in status_string.go,
// which go generate writes.
type Status int
const (
Pending Status = iota
Paid
Shipped
Cancelled
)
GO
mkdir gen
cat >gen/main.go <<'GO'
// Command gen writes status_string.go: a String method for Status, from the
// names listed here. It stands in for stringer, which lives outside std.
package main
import (
"fmt"
"os"
)
func main() {
names := []string{"Pending", "Paid", "Shipped", "Cancelled"}
out, err := os.Create("status_string.go")
if err != nil {
panic(err)
}
defer out.Close()
fmt.Fprintf(out, "// Code generated by gen from %s:%s; DO NOT EDIT.\n\n", os.Getenv("GOFILE"), os.Getenv("GOLINE"))
fmt.Fprintf(out, "package %s\n\n", os.Getenv("GOPACKAGE"))
fmt.Fprintln(out, `import "fmt"`)
fmt.Fprintln(out)
fmt.Fprintln(out, "func (s Status) String() string {")
fmt.Fprintln(out, "\tswitch s {")
for _, name := range names {
fmt.Fprintf(out, "\tcase %s:\n\t\treturn %q\n", name, name)
}
fmt.Fprintln(out, "\t}")
fmt.Fprintln(out, ` return fmt.Sprintf("Status(%d)", int(s))`)
fmt.Fprintln(out, "}")
}
GO
cat >main.go <<'GO'
package main
import "fmt"
func main() {
for s := Pending; s <= Cancelled+1; s++ {
fmt.Println(int(s), s.String())
}
}
GO
say 'gofmt -l .'
gofmt -l .
say 'go build -o orders .'
go build -o orders . 2>&1
echo "exit status $?"
say 'go generate -x ./...'
go generate -x ./... 2>&1
echo "exit status $?"
say 'cat status_string.go'
cat status_string.go
say 'gofmt -l status_string.go'
gofmt -l status_string.go
say 'go build -o orders . && ./orders'
go build -o orders . && ./orders
go_generate_env_and_run_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# What go generate hands the command: $GOFILE, $GOPACKAGE and $GOLINE name the
# directive's place, $DOLLAR is a literal $. A directive must start at column
# 0 with no space after //. -n prints commands without running them, -run
# picks a subset, a command that fails stops the run, and a file the build
# would leave out is left out here too -- unless its tag is "generate".
set -u
export GOTOOLCHAIN=local
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
cd "$dir" || exit 1
say() { printf '$ %s\n' "$*"; }
go mod init example >/dev/null 2>&1 || exit 1
cat >orders.go <<'GO'
package orders
//go:generate echo file=$GOFILE package=$GOPACKAGE line=$GOLINE literal=$DOLLAR
GO
cat >invoices.go <<'GO'
package orders
//go:generate echo file=$GOFILE line=$GOLINE
// go:generate echo never printed: a space after //
GO
cat >tagged.go <<'GO'
//go:build generate
package orders
//go:generate echo from tagged.go, which go build never compiles
GO
cat >skipped.go <<'GO'
//go:build never
package orders
//go:generate echo never printed: skipped.go is excluded by its constraint
GO
say 'go generate -n ./...'
go generate -n ./... 2>&1
say 'go generate ./...'
go generate ./... 2>&1
say 'go generate -run literal ./...'
go generate -run literal ./... 2>&1
say "go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' ."
go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' .
cat >failing.go <<'GO'
package orders
//go:generate false
GO
say 'go generate ./...'
go generate ./... 2>&1
echo "exit status $?"
What to do¶
- Commit the generated files, and have CI run
go generate ./...followed bygit diff --exit-code: that is the only check that a generated file is current, because the build cannot tell. - Put the directive in the file that declares the type, write the
// Code generated … DO NOT EDIT.line first, and name the generator and its input in it. - Prefer
go run ./genor ago.modtooldirective to a binary someone has to install; a generator in the module is versioned with the code it generates. - Format the output with
go/format.Sourcein the generator, so thatgofmt -lstays quiet without a second directive. - Keep a generator's own directives in a
//go:build generatefile when they must not be compiled into the package.
Practice¶
Write the generator itself as one program: emit a String() method for a Priority type with the values Low, Normal and Urgent into a buffer, deliberately unindented; format the text with go/format the way gofmt would; check the first line against the regular expression from go help generate; and print the file you would have written, with a line saying whether formatting changed anything.
Solution
go_generate_runs_a_command_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// Kata: write the generator itself as one program. It emits a String method
// for a Priority type into a buffer, formats the text with go/format the way
// gofmt would, checks the first line against the regular expression that
// go help generate gives for generated files, and prints the file it would
// have written.
//
// go build -trimpath go_generate_runs_a_command_kata_go.go && ./go_generate_runs_a_command_kata_go
package main
import (
"bytes"
"fmt"
"go/format"
"regexp"
)
// generatedHeader is the pattern from go help generate, which gofmt, go vet
// and editors use to recognise a generated file.
var generatedHeader = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`)
func main() {
names := []string{"Low", "Normal", "Urgent"}
var src bytes.Buffer
fmt.Fprintln(&src, "// Code generated by go_generate_runs_a_command_kata_go; DO NOT EDIT.")
fmt.Fprintln(&src)
fmt.Fprintln(&src, "package orders")
fmt.Fprintln(&src)
fmt.Fprintln(&src, `import "fmt"`)
fmt.Fprintln(&src)
fmt.Fprintln(&src, "func (p Priority) String() string {")
fmt.Fprintln(&src, "switch p {") // unindented on purpose: go/format fixes it
for _, name := range names {
fmt.Fprintf(&src, "case %s: return %q\n", name, name)
}
fmt.Fprintln(&src, "}")
fmt.Fprintln(&src, `return fmt.Sprintf("Priority(%d)", int(p))`)
fmt.Fprintln(&src, "}")
formatted, err := format.Source(src.Bytes())
if err != nil {
panic(err)
}
firstLine, _, _ := bytes.Cut(formatted, []byte("\n"))
fmt.Println("header recognised:", generatedHeader.Match(firstLine))
fmt.Println("go/format changed the text:", !bytes.Equal(formatted, src.Bytes()))
fmt.Println("---")
fmt.Print(string(formatted))
}
Verified output of go_generate_runs_a_command_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.
header recognised: true
go/format changed the text: true
---
// Code generated by go_generate_runs_a_command_kata_go; DO NOT EDIT.
package orders
import "fmt"
func (p Priority) String() string {
switch p {
case Low:
return "Low"
case Normal:
return "Normal"
case Urgent:
return "Urgent"
}
return fmt.Sprintf("Priority(%d)", int(p))
}
In other languages¶
- The Rust library's Build scripts:
build.rs↗ — Cargo compiles and runsbuild.rsbefore every build, automatically, and the generated code lands inOUT_DIRand is pulled in withinclude!; Go's step is explicit, and its output is an ordinary committed file. - C generates code through the build graph: a
makerule whose target is the generated file and whose prerequisite is the input reruns the generator when the input is newer — the C library's Makefiles ↗.go generatehas no timestamps and no dependency graph; it runs what it is told, every time.
Sources¶
- Generate Go files by processing source, in the go command documentation ↗ (
go help generate) — the directive syntax, the variables, thegeneratetag and the header pattern. - Rob Pike, Generating code ↗, on the Go blog (2014) — the design, with
stringeras its example. - Issue 13560 ↗ — where the
// Code generated … DO NOT EDIT.convention was agreed. stringer↗, the tool this page's generator stands in for, and the Go wiki's list of generators ↗.- The Go 1.24 release notes ↗ — the
tooldirective ingo.mod. - Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 11, "Go Tooling" — the sections "Using go generate" and "Working with go generate and Makefiles".
- Go in Practice, 2nd ed. (Manning, 2025), chapter 13, "Reflection, code generation, and advanced Go" — section 13.3, "Generating Go code with Go code".
- Miki Tebeka, Effective Go Recipes (Pragmatic, 2024), chapter 14, "Building Applications" — Recipe 77, "Generating Code".