go generate & stringer
go generate orchestrates code generators.
stringer is the hello-world generator: typed constants get a correct String() method without reflection.
Summary
Place //go:generate comments above functions or types in any .go file.
Run go generate ./... from module root to execute those commands.
stringer emits a *_string.go file with String() for int-based enums.
Teams commit generated output and rerun generators when types change.
Recipe
Quick-reference generate workflow.
//go:generate stringer -type=Status -trimprefix=Status
type Status int
const (
StatusPending Status = iota
StatusActive
StatusDone
)go install golang.org/x/tools/cmd/stringer@latest
go generate ./...
go test ./...When to reach for this:
- Enums logged in metrics, APIs, or UI need stable string names
- You want compile-time
String()without maintaining switch statements by hand - CI should fail when generated files drift from source types
- You are introducing any
go generatetool (mockgen, protobuf, stringer)
Working Example
Full mini-module with stringer output checked in.
// status.go
package job
//go:generate stringer -type=Status -trimprefix=Status
type Status int
const (
StatusPending Status = iota
StatusRunning
StatusFailed
StatusSucceeded
)
func (s Status) IsTerminal() bool {
return s == StatusFailed || s == StatusSucceeded
}After go generate:
// status_string.go (generated)
package job
import "strconv"
func _() {
_ = x[StatusPending-0]
}
const _Status_name = "PendingRunningFailedSucceeded"
var _Status_index = [...]uint8{0, 7, 14, 20, 29}
func (i Status) String() string {
if i < 0 || i >= Status(len(_Status_index)-1) {
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Status_name[_Status_index[i]:_Status_index[i+1]]
}// job_test.go
package job
import "testing"
func TestStatusString(t *testing.T) {
if StatusRunning.String() != "Running" {
t.Fatal(StatusRunning.String())
}
}What this demonstrates:
-trimprefixremoves redundantStatusprefix from string output- Generated file lives beside source with
DO NOT EDITheader (stringer adds it) - Invalid numeric values get a safe
Status(99)fallback string - Tests lock behavior without reflection
Deep Dive
How go generate Works
- Scans
.gofiles for lines starting with//go:generate - Runs each command with
$GOFILEand$GOPACKAGEset to the containing file - Does not analyze dependencies; order follows file walk order
- Failures stop that package's remaining directives unless you script otherwise
Common Generators
| Tool | Directive example | Output |
|---|---|---|
| stringer | //go:generate stringer -type=T | t_string.go |
| mockgen | //go:generate mockgen -source=iface.go | mock_iface.go |
| protobuf | //go:generate protoc --go_out=. api.proto | api.pb.go |
| enumer | third-party enum helpers | custom |
CI Check for Drift
go generate ./...
git diff --exit-code || (echo "run go generate and commit"; exit 1)Go Notes
// Keep generators in tools module or tools.go with build tag
//go:build tools
package tools
import _ "golang.org/x/tools/cmd/stringer"Gotchas
- Expecting go build to run generate - It will not. Fix: Makefile, CI step, or pre-commit hook.
- Editing generated files - Next generate overwrites fixes. Fix: Change generator flags or template inputs.
- stringer after renaming constants - Stale strings until regenerate. Fix: Add generate+diff CI gate.
- Multiple types one file - Each needs its own directive or
-typelist. Fix: Separate//go:generatelines per type. - Vendoring stringer - Pin
golang.org/x/toolsversion intools.gofor reproducible CI. Fix:go installat pinned module version in CI. - Generated files in code review noise - Large diffs on enum tweaks. Fix: Dedicated commits for regeneration.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Hand-written String() | One or two constants | Large enums change often |
fmt.Sprintf("%d") | Debug only | Logs and APIs need names |
| iota + map | Tiny enums without tooling | You already use stringer elsewhere |
| protobuf enums | gRPC services | Plain Go domain types |
FAQs
Does go generate run on go test?
No.
Invoke it explicitly in CI or local scripts.
Should I commit generated files?
Most Go projects yes, so consumers build without generator tools.
Document the regenerate command in README or CONTRIBUTING.
What does -trimprefix do?
Strips a prefix from constant names in the generated string.
StatusRunning becomes "Running" with -trimprefix=Status.
Can stringer handle multiple types?
Yes: stringer -type=Foo,Bar or multiple directives.
Where do generate directives live?
Any .go file in the package, commonly next to the type definition.
How do I pin stringer version?
Use a tools.go with build tag tools and install that module version in CI.
What if generate fails in CI?
Fix the generator command, missing protoc plugins, or PATH.
Log $GOFILE for the failing directive.
Is stringer reflection-based?
No.
It is a compile-time code generator using go/types.
Can I use go generate with go fix?
Yes.
Different purposes: generate creates files; fix modernizes call sites.
Run both on upgrade branches.
How do I test generated String methods?
Table-test known constants and one out-of-range value.
Related
- Building Custom Code Generators - write your own generator
- Reflection vs Code Generation in Go - codegen vs reflect
- go:fix inline & API Migration Directives - migration tooling
- Reflection & Code Generation Best Practices - team workflow rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).