Code Generation Advanced¶
Introduction¶
Code generation is a first-class strategy in Go for eliminating boilerplate, enforcing type safety, and creating performant serialization — all before your code compiles. Instead of relying on runtime reflection (slow, untyped) or generics (limited scope), Go encourages generating concrete, type-safe code at development time via go generate.
The ecosystem is built on this principle: stringer for enum strings, mockgen for test mocks, protoc for gRPC, sqlc for type-safe SQL, and ent for ORM code. Understanding code generation is essential for building production Go systems.
Syntax & Usage¶
The go generate Directive¶
go generate scans Go files for special comments and runs the specified commands. It is not invoked automatically by go build — you run it explicitly.
//go:generate stringer -type=Color
//go:generate mockgen -source=repo.go -destination=mock_repo.go
package mypackage
# Run all generators in current package
go generate ./...
# Run generators in specific package
go generate ./internal/models/...
# Verbose output
go generate -v ./...
# Run only generators matching a regex
go generate -run "stringer" ./...
go generate runs at development time
go generate is a developer tool, not part of the build process. Generated files should be committed to version control so that go build works without running generators.
stringer — Enum String Methods¶
The stringer tool generates String() methods for integer-based enums, implementing fmt.Stringer.
package status
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Suspended
Closed
)
Running go generate produces status_string.go:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
func (i Status) String() string {
switch i {
case Pending:
return "Pending"
case Active:
return "Active"
case Suspended:
return "Suspended"
case Closed:
return "Closed"
}
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
Install with: go install golang.org/x/tools/cmd/stringer@latest
mockgen — Test Mock Generation¶
mockgen generates mock implementations of interfaces for use in unit tests.
package repository
//go:generate mockgen -source=user_repo.go -destination=mock_user_repo.go -package=repository
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
}
Using the generated mock in tests:
func TestGetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
mockRepo.EXPECT().
GetByID(gomock.Any(), int64(42)).
Return(&User{ID: 42, Name: "Alice"}, nil)
svc := NewUserService(mockRepo)
user, err := svc.GetUser(context.Background(), 42)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
protoc — Protocol Buffers and gRPC¶
Protocol buffer definitions generate Go structs and gRPC service stubs:
// proto/user.proto
syntax = "proto3";
option go_package = "myapp/pb";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
int64 id = 1;
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
AST Manipulation — Writing Custom Generators¶
Go provides go/ast, go/parser, go/printer, and go/token for parsing and manipulating Go source code programmatically.
package main
import (
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"strings"
)
func main() {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "input.go", nil, parser.ParseComments)
if err != nil {
panic(err)
}
// Find all exported struct types
ast.Inspect(file, func(n ast.Node) bool {
ts, ok := n.(*ast.TypeSpec)
if !ok || !ts.Name.IsExported() {
return true
}
if _, ok := ts.Type.(*ast.StructType); ok {
// Generate code for this struct (e.g., JSON marshaler, validator)
generateForStruct(ts)
}
return true
})
printer.Fprint(os.Stdout, fset, file)
}
Practical Custom Generator — JSON Enum Marshaler¶
A complete generator that creates JSON marshal/unmarshal methods for enums:
// cmd/enumjson/main.go
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"strings"
"text/template"
)
var typeName = flag.String("type", "", "type name for enum")
const tmpl = `// Code generated by enumjson; DO NOT EDIT.
package {{.Package}}
import "encoding/json"
var _{{.Type}}NameMap = map[{{.Type}}]string{
{{- range .Values}}
{{.}}: "{{.}}",
{{- end}}
}
var _{{.Type}}ValueMap = map[string]{{.Type}}{
{{- range .Values}}
"{{.}}": {{.}},
{{- end}}
}
func (e {{.Type}}) MarshalJSON() ([]byte, error) {
if name, ok := _{{.Type}}NameMap[e]; ok {
return json.Marshal(name)
}
return nil, fmt.Errorf("invalid {{.Type}}: %d", e)
}
func (e *{{.Type}}) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if val, ok := _{{.Type}}ValueMap[s]; ok {
*e = val
return nil
}
return fmt.Errorf("invalid {{.Type}}: %q", s)
}
`
func main() {
flag.Parse()
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, ".", nil, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "parse error: %v\n", err)
os.Exit(1)
}
var pkgName string
var values []string
for name, pkg := range pkgs {
pkgName = name
for _, file := range pkg.Files {
for _, decl := range file.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.CONST {
continue
}
for _, spec := range gd.Specs {
vs := spec.(*ast.ValueSpec)
if ident, ok := vs.Type.(*ast.Ident); ok && ident.Name == *typeName {
values = append(values, vs.Names[0].Name)
}
// Handle iota group where type is specified only on the first const
if vs.Type == nil && len(values) > 0 {
values = append(values, vs.Names[0].Name)
}
}
}
}
}
t := template.Must(template.New("enum").Parse(tmpl))
var buf bytes.Buffer
t.Execute(&buf, map[string]any{
"Package": pkgName,
"Type": *typeName,
"Values": values,
})
outFile := strings.ToLower(*typeName) + "_json.go"
os.WriteFile(outFile, buf.Bytes(), 0644)
}
Usage in your source file:
//go:generate go run ./cmd/enumjson -type=Priority
type Priority int
const (
Low Priority = iota
Medium
High
Critical
)
text/template for Code Generation¶
Go's text/template is the standard engine for code generators:
const structTemplate = `// Code generated; DO NOT EDIT.
package {{.Package}}
{{range .Structs}}
func (s *{{.Name}}) Validate() error {
{{- range .Fields}}
{{- if .Required}}
if s.{{.Name}} == {{.ZeroValue}} {
return fmt.Errorf("{{.Name}} is required")
}
{{- end}}
{{- end}}
return nil
}
{{end}}
`
go generate vs Generics vs Reflection¶
| Approach | When to Use | Pros | Cons |
|---|---|---|---|
| go generate | Boilerplate, serialization, mocks | Type-safe, zero runtime cost, full flexibility | Extra build step, generated code to maintain |
| Generics | Algorithms, containers, utility functions | No build step, compiler-checked | Limited constraints, no method generation |
| Reflection | Dynamic dispatch, generic marshaling | No build step, fully dynamic | Slow, no type safety, complex code |
Quick Reference¶
| Tool / Package | Purpose | Install |
|---|---|---|
go generate |
Run generator commands from comments | Built-in |
stringer |
String() for integer enums |
golang.org/x/tools/cmd/stringer |
mockgen |
Interface mock generation | go.uber.org/mock/mockgen |
protoc-gen-go |
Protocol buffer Go structs | google.golang.org/protobuf/cmd/protoc-gen-go |
protoc-gen-go-grpc |
gRPC service stubs | google.golang.org/grpc/cmd/protoc-gen-go-grpc |
sqlc |
Type-safe SQL → Go | github.com/sqlc-dev/sqlc |
go/ast |
Parse Go source into AST | Standard library |
go/parser |
Parse Go files and packages | Standard library |
go/printer |
Pretty-print AST back to source | Standard library |
text/template |
Template-based generation | Standard library |
Best Practices¶
- Commit generated files —
go generateis a dev tool, not a build step. CI should not need to run generators; committing ensuresgo buildalways works. - Add
// Code generated ... DO NOT EDIT.header — Go tooling recognizes this header and excludes generated files from certain linters and reviews. - Create a top-level
generate.go— centralize//go:generatedirectives in one file per package for discoverability. -
Pin generator versions — use
tools.gowith a build constraint to track generator dependencies ingo.mod: -
Verify generated code is fresh in CI — run
go generate ./...and thengit diff --exit-codeto catch stale generated files. - Prefer generation over reflection — generated code is type-safe, has zero runtime overhead, and is debuggable. Use reflection only when the type is truly unknown at compile time.
Common Pitfalls¶
Generated files not committed to version control
# CI fails because generated files aren't checked in
$ go build ./...
# error: undefined: StatusString (stringer output missing)
go generate, you need all generator tools installed in the CI image — fragile and slow.
Generator directive placement
// WRONG: directive must be a comment in a .go file
# This won't work — go generate ignores non-.go files
// WRONG: must be exactly "//go:generate" with no space before "go"
// go:generate stringer -type=Status
//go:generate (no space after //) in a .go file.
Stale generated code
When you add a new enum value but forget to re-run go generate, the generated String() method won't include it. Add a CI check:
Circular dependencies in generators
If your generator imports the package it generates code for, you get a circular dependency. Keep generators in a separate cmd/ directory that only reads source files — never imports the target package.
Performance Considerations¶
- Zero runtime cost: Generated code is compiled like hand-written code — no reflection, no interface boxing, no dynamic dispatch. This is the primary advantage over reflection.
- Build time:
go generateruns external tools, which can be slow (especiallyprotoc). Run generators only when source files change, not on every build. - Binary size: Generated code increases binary size proportionally to the number of types. For large protobuf schemas, this can be significant — consider splitting packages.
- Stringer vs fmt.Sprintf: A generated
String()method is a simple switch statement — O(1). The reflection-based alternative (fmt.Sprintf("%d", val)) is orders of magnitude slower for enum-to-string conversion. - mockgen overhead: Generated mocks are verbose but have negligible impact on test binary size. The alternative — hand-written mocks — is just as large and more error-prone.
Interview Tips¶
Interview Tip
"What is go generate and when would you use it?" go generate is a development-time tool that scans Go source files for //go:generate comments and runs the specified commands. It's used for generating string methods for enums, creating test mocks, generating protobuf/gRPC code, and building type-safe serializers. Generated files are committed to VCS — go generate is never part of the build itself.
Interview Tip
"When would you use code generation vs generics vs reflection?" Code generation for boilerplate that needs type-specific logic (serializers, validators, mocks). Generics for algorithms and containers that work across types with shared constraints. Reflection only as a last resort when types are truly unknown at compile time (think encoding/json). Generation gives zero runtime cost and full type safety.
Interview Tip
"How do you write a custom code generator in Go?" Parse source files with go/parser to get an AST, walk it with ast.Inspect to find target types, extract metadata (fields, types, tags), then use text/template to emit Go source code with the // Code generated ... DO NOT EDIT. header. Wire it up with a //go:generate directive.
Interview Tip
"How do you ensure generated code stays fresh?" In CI, run go generate ./... followed by git diff --exit-code. If there's a diff, the generated files are stale and the build should fail. Pin generator tool versions in a tools.go file tracked by go.mod to prevent version drift.
Key Takeaways¶
go generateruns commands from//go:generatecomments — it's a dev tool, not a build step.- Always commit generated files and add the
// Code generated ... DO NOT EDIT.header. stringergeneratesString()for enums,mockgencreates test mocks,protocgenerates gRPC stubs.- Write custom generators using
go/ast+go/parserfor AST manipulation andtext/templatefor output. - Code generation produces type-safe, zero-overhead code — prefer it over reflection for known-at-compile-time types.
- Pin generator versions in
tools.goand verify freshness in CI withgit diff --exit-code. - The trade-off: generated code adds files to maintain but eliminates runtime cost and type-safety gaps.