Go Advanced #7 Code Generation — go generate and stringer
If #6 Profiling showed measurement tools — this post is about a case that’s often discovered through measurement. How to make hot-path reflect fast.
Go’s answer is — code generation. Instead of analyzing with reflect at runtime, generate the code ahead of time at compile time. The final post of the Advanced series.
The picture of go generate
#
go generate — a tool that runs commands when it sees special comments in your source.
//go:generate stringer -type=Status
package main
type Status int
const (
Pending Status = iota
Active
Closed
)go generate ./...This command — finds the comment above and runs stringer -type=Status. That tool generates a new file.
Result — auto-generated file #
The status_string.go produced by stringer:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
package main
func (i Status) String() string {
switch i {
case Pending:
return "Pending"
case Active:
return "Active"
case Closed:
return "Closed"
}
return "Status(...)"
}Now Status — satisfies fmt.Stringer. fmt.Println(s) produces a human-readable string like “Active”.
Why is it better than reflect? #
You can do the same with reflect — resolving enum names dynamically — but:
| Comparison | reflect | code gen |
|---|---|---|
| Speed | ~100x slower | same as ordinary code |
| Compile-time check | none | yes |
| Debugging | hard | easy |
| Build complexity | simple | adds a generate step |
When hot-path reflect can be solved with compile-time information — code generation is always better.
stringer — enum string representation #
go install golang.org/x/tools/cmd/stringer@latest//go:generate stringer -type=Status,Priority
type Status int
const (
Pending Status = iota
Active
)
type Priority int
const (
Low Priority = iota
High
)Generated files: status_string.go, priority_string.go. Whenever an enum is added — one go generate regenerates them.
mockgen — interface mocking #
go.uber.org/mock’s mockgen — auto-generates a mock from an interface.
//go:generate mockgen -source=db.go -destination=mock_db.go -package=mocks
type DB interface {
Get(id string) (User, error)
Save(u User) error
}With the generated mock — you can predefine behavior in tests.
mockDB := mocks.NewMockDB(ctrl)
mockDB.EXPECT().Get("u1").Return(User{Name: "Dokyung Lee"}, nil)
svc := NewService(mockDB)
svc.Greet("u1")You could build dynamic mocks with reflect — but generation is better for IDE autocomplete, compile-time type checking, and debugging.
Building your own — with text/template
#
A common pattern.
//go:build ignore
package main
import (
"os"
"text/template"
)
const tmpl = `// DO NOT EDIT.
package main
func {{.Name}}Greet() string {
return "Hello, {{.Name}}!"
}
`
func main() {
t := template.Must(template.New("g").Parse(tmpl))
f, _ := os.Create("greet_gen.go")
t.Execute(f, map[string]string{"Name": "World"})
}//go:generate go run generator.goThe //go:build ignore tag — excludes it from normal builds and runs only via go run generator.go.
Pattern — accelerating serialization #
A common case in larger projects. encoding/json’s reflect cost — bypassed via code generation.
Tools:
mailru/easyjson— struct → dedicated marshaller codepquerna/ffjson— similar conceptmvdan/gogenerate,vektra/mockery— across various domains
//go:generate easyjson -all user.go
//easyjson:json
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}Generated user_easyjson.go — User-specific MarshalJSON / UnmarshalJSON. Handles bytes directly without reflect, so it’s fast.
Benchmark results — sometimes 2–5x faster than encoding/json. The tradeoff is — extra build step and auto-generated code mixed into PRs.
protobuf / gRPC — the apex of auto-generation #
protoc --go_out=. user.protoFrom a .proto file — Go structs, marshallers, gRPC server/client code all auto-generated. The biggest code-generation case you’ll meet in practice.
user.pb.go ← struct + Marshal/Unmarshal
user_grpc.pb.go ← gRPC server/clientTens of thousands of lines — from a single .proto. This pipeline is a standard tool for microservices.
Practical workflow #
Tie it into a Makefile #
.PHONY: gen
gen:
go generate ./...
.PHONY: build
build: gen
go build ./...
Generation as a pre-build step. Auto-generated files are usually committed to git (so other developers can build without the tools).
Verify in CI #
That generated files — are always up to date.
go generate ./...
git diff --exit-code # fail if there are changes after generateIf a PR is missing a new auto-generated file, CI catches it.
How far to auto-generate #
Avoid:
- Code with frequently changing logic — the generate step becomes a burden
- Large generated files that make PR review hard — real changes are buried
- Small projects where external-tool dependencies are a burden
Natural fit:
- String methods on enums, JSON marshallers — based on static information
- Interface mocks
- Spec → code transformations like protobuf / OpenAPI
The rule is simple — once you’ve written the same pattern by hand 5+ times, it’s time to consider generation.
Pitfall — editing generated files directly #
// Code generated by ...; DO NOT EDIT.When you see this header — never edit directly. The next generate run will overwrite it. If you really need to change it — modify the generator tool or the template.
Pitfall — pin generator tool versions #
//go:build tools
// +build tools
package tools
import (
_ "golang.org/x/tools/cmd/stringer"
_ "go.uber.org/mock/mockgen"
)In tools.go — import the tools you use (excluded from normal builds via build tag). go.mod pins versions. Guarantees the whole team uses the same tool versions.
Series wrap-up — looking back at 7 Advanced posts #
Summarizing the 7 posts:
- Concurrency patterns — pipeline, fan-out/fan-in, semaphore
- Memory model and sync — Mutex, atomic, Once
- Generics — type parameter, constraint
- reflect — handling types at runtime
- unsafe and cgo — outside the safe zone
- Profiling — pprof, benchmark, trace
- Code generation (this post)
These are cases where the limits of the tools from Basics/Intermediate become visible — patterns for solid concurrency, synchronization of shared memory, polymorphism via generics, boundary crossing with reflect/unsafe/cgo, measurement and automation. Where these tools all show up together is usually framework or library code.
Next steps #
The next series (#1 First HTTP Server) is Go Practice in 6 posts. We put the tools we’ve learned to work by building an actual backend — from net/http, ServeMux, JSON, and DB integration to middleware, testing, and deployment.
Wrap-up #
What we covered:
//go:generate+go generate ./...— the standard for compile-time code generation- stringer — String method for enums
- mockgen — interface mocks
- easyjson / protoc — serialization acceleration, RPC code
- Auto-generated files — committed to git, never edited directly, freshness verified in CI
- Tool versions pinned via
tools.go - If reflect is on the hot path — consider code generation
That wraps up the 7 Advanced posts.