Code Generation
Why Code Generation Matters
Code generation is essential in Go because it eliminates boilerplate, ensures consistency, generates type-safe code from schemas, and automates repetitive tasks. Go’s go generate directive and ecosystem of generators make code generation a first-class development tool.
Core benefits:
- Eliminate boilerplate: Generate repetitive code automatically
- Type safety: Generate from schemas (Protocol Buffers, OpenAPI)
- Consistency: Generated code follows exact patterns
- Productivity: Focus on business logic, not plumbing code
Problem: Without code generation, developers write repetitive boilerplate code (String() methods, mocks, database queries), increasing errors and maintenance burden.
Solution: Use go:generate directive to automate code generation during development, starting with built-in tools before adding external generators.
Standard Library: go generate
Go includes the go generate command to invoke code generators.
Basic go generate usage:
// File: status.go
package main
import "fmt"
//go:generate echo "Running code generation"
// => go:generate directive (special comment)
// => No space between // and go:generate
// => Runs command when "go generate" executed
type Status int
// => Status is enum-like type
// => int underlying type
const (
// => Enum values using iota
StatusPending Status = iota
// => StatusPending = 0
StatusRunning
// => StatusRunning = 1
StatusComplete
// => StatusComplete = 2
)
func main() {
// => Entry point for demonstration
fmt.Println(StatusPending)
// => Output: 0 (not human-readable)
// => Need String() method for readability
}Running go generate:
go generate
# => Scans Go files for //go:generate directives
# => Executes commands in order
# => Output: Running code generation
go generate ./...
# => Runs generators for all packages recursively
# => Common in CI/CD pipelinesEnvironment variables available in go:generate:
//go:generate echo $GOFILE $GOLINE $GOPACKAGE $DOLLAR
// => $GOFILE: Current filename (status.go)
// => $GOLINE: Line number of directive
// => $GOPACKAGE: Package name (main)
// => $DOLLAR: Literal $ (for shell scripts)Limitations of manual code generation:
- Must write custom generator scripts
- No built-in generators for common patterns
- Complex generators require separate tools
Production Tool: stringer (Enum String Generation)
stringer generates String() methods for enum types automatically.
Installation:
go install golang.org/x/tools/cmd/stringer@latest
# => Installs stringer tool
# => Adds to $GOPATH/bin or $GOBIN
# => Requires Go 1.17+Using stringer:
// File: status.go
package main
//go:generate stringer -type=Status
// => Generates String() method for Status type
// => Creates status_string.go file
// => stringer reads this file, generates methods
type Status int
// => Enum type for stringer
const (
StatusPending Status = iota
// => StatusPending = 0
StatusRunning
// => StatusRunning = 1
StatusComplete
// => StatusComplete = 2
StatusFailed
// => StatusFailed = 3
)
func main() {
// => Demonstrates stringer output
s := StatusRunning
// => s is StatusRunning (value 1)
fmt.Println(s.String())
// => Output: StatusRunning (human-readable)
// => String() method generated by stringer
}Running stringer:
go generate
# => Executes //go:generate stringer directive
# => Creates status_string.go with String() methodGenerated code (status_string.go):
// Code generated by "stringer -type=Status"; DO NOT EDIT.
// => Warning comment: don't manually edit
// => Regenerated when go generate runs
package main
import "strconv"
func _() {
// => Compile-time check: ensures enum values match
// => Causes compile error if enum changes
var x [1]struct{}
_ = x[StatusPending-0]
_ = x[StatusRunning-1]
_ = x[StatusComplete-2]
_ = x[StatusFailed-3]
}
const _Status_name = "StatusPendingStatusRunningStatusCompleteStatusFailed"
// => Concatenated string of all enum names
// => Offsets stored in _Status_index
var _Status_index = [...]uint8{0, 13, 26, 40, 52}
// => Byte offsets into _Status_name
// => [0:13] = "StatusPending"
// => [13:26] = "StatusRunning"
// => etc.
func (i Status) String() string {
// => Generated String() method
// => i is receiver (Status value)
if i < 0 || i >= Status(len(_Status_index)-1) {
// => Bounds check for invalid values
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
// => Returns "Status(99)" for invalid value
}
return _Status_name[_Status_index[i]:_Status_index[i+1]]
// => Slices concatenated string using offsets
// => Returns human-readable name
}stringer benefits:
- Human-readable enum output
- Compile-time validation
- Minimal runtime overhead (no reflection)
- JSON marshaling support
Trade-offs:
| Approach | Pros | Cons |
|---|---|---|
| Manual String() methods | No dependencies, full control | Verbose, error-prone, maintenance |
| stringer | Automatic, consistent, compile-time safe | External tool, generated code |
When to use:
- Manual: 1-2 enum types
- stringer: 3+ enum types, especially public APIs
Production Tool: mockgen (Mock Generation for Testing)
mockgen generates test mocks from interfaces automatically.
Installation:
go install go.uber.org/mock/mockgen@latest
# => Installs mockgen tool
# => Replaces deprecated golang/mockSource mode (generate from interface):
// File: repository.go
package repository
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repository
// => Generates mocks from this file
// => -source: input file with interfaces
// => -destination: output file for mocks
// => -package: keeps mocks in same package
type UserRepository interface {
// => Interface to mock
FindByID(id int) (*User, error)
// => Method to generate mock for
Save(user *User) error
// => Another method to mock
}
type User struct {
// => User model
ID int
Name string
}Generated mock (mock_repository.go):
// Code generated by MockGen. DO NOT EDIT.
package repository
import (
gomock "go.uber.org/mock/gomock"
// => mockgen imports gomock for mock infrastructure
)
type MockUserRepository struct {
// => Generated mock struct
ctrl *gomock.Controller
// => Controller tracks expectations
recorder *MockUserRepositoryMockRecorder
// => Recorder for method expectations
}
func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
// => Constructor for mock
// => ctrl from gomock.NewController(t)
mock := &MockUserRepository{ctrl: ctrl}
mock.recorder = &MockUserRepositoryMockRecorder{mock}
return mock
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
// => Generated mock method
// => Delegates to mock controller
m.ctrl.T.Helper()
// => Marks as test helper
ret := m.ctrl.Call(m, "FindByID", id)
// => Records method call with arguments
// => Returns configured values
ret0, _ := ret[0].(*User)
// => First return value (User pointer)
ret1, _ := ret[1].(error)
// => Second return value (error)
return ret0, ret1
// => Returns configured values
}
func (m *MockUserRepositoryMockRecorder) FindByID(id interface{}) *gomock.Call {
// => Expectation recorder method
// => Used in tests to set expectations
return m.mock.ctrl.RecordCallWithMethodType(
m.mock, "FindByID", reflect.TypeOf((*MockUserRepository)(nil).FindByID), id)
// => Records expectation
}Using generated mocks:
// File: service_test.go
package repository
import (
"testing"
"go.uber.org/mock/gomock"
// => mockgen mock infrastructure
)
func TestUserService_GetUser(t *testing.T) {
// => Test with generated mock
// => Verifies service behavior without real database
ctrl := gomock.NewController(t)
// => Creates mock controller
// => Tracks mock expectations
defer ctrl.Finish()
// => Verifies all expectations met
// => Fails test if expected calls not made
mockRepo := NewMockUserRepository(ctrl)
// => Creates mock from generated code
// => Ready to set expectations
expectedUser := &User{ID: 1, Name: "Alice"}
// => Test fixture data
mockRepo.EXPECT().
FindByID(1).
// => Expects FindByID called with argument 1
Return(expectedUser, nil).
// => Configures return values
Times(1)
// => Expects exactly one call
// => Times() is optional (default: 1)
// Test service using mock
service := NewUserService(mockRepo)
// => Service depends on UserRepository interface
// => Mock implements interface
user, err := service.GetUser(1)
// => Calls service method
// => Service calls mockRepo.FindByID(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got %s, want Alice", user.Name)
}
// ctrl.Finish() verifies expectations when test ends
}mockgen benefits:
- Type-safe mocks
- Automatic expectation verification
- No manual mock maintenance
- Supports complex interface hierarchies
Production Tool: protoc (Protocol Buffers)
Protocol Buffers generate type-safe structs and serialization from .proto schemas.
Installation:
# Install protoc compiler
brew install protobuf # macOS
# Or download from github.com/protocolbuffers/protobuf/releases
# Install Go plugin
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# => Generates Go code from .proto files
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# => Generates gRPC service code (if needed)Proto schema definition:
// File: user.proto
syntax = "proto3";
// => Protocol Buffers version 3
// => Required first line
package user;
// => Package name (namespace)
option go_package = "myproject/internal/pb/user";
// => Go import path for generated code
message User {
// => Message definition (becomes Go struct)
int32 id = 1;
// => Field number 1
// => int32 type (32-bit integer)
// => Number is wire format identifier (never changes)
string name = 2;
// => Field number 2
// => string type
string email = 3;
// => Field number 3
}
message GetUserRequest {
// => Request message for RPC
int32 user_id = 1;
}
message GetUserResponse {
// => Response message for RPC
User user = 1;
// => Nested User message
}Generate Go code:
//go:generate protoc --go_out=. --go_opt=paths=source_relative user.proto
// => Generates user.pb.go from user.proto
// => --go_out=.: output to current directory
// => --go_opt=paths=source_relative: relative import pathsgo generate
# => Runs protoc via go:generate
# => Creates user.pb.goGenerated code (user.pb.go excerpt):
// Code generated by protoc-gen-go. DO NOT EDIT.
package user
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
// => Protobuf reflection support
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
// => Protobuf runtime
)
type User struct {
// => Generated struct from proto message
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
// => int32 field from proto
// => protobuf tag for wire format
// => json tag for JSON marshaling
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
// => string field
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
}
func (x *User) GetId() int32 {
// => Generated getter
// => Safe access even if x is nil
if x != nil {
return x.Id
}
return 0
}Using generated protocol buffers:
package main
import (
"fmt"
"myproject/internal/pb/user"
// => Import generated package
"google.golang.org/protobuf/proto"
// => Protobuf marshaling
)
func main() {
// => Demonstrates protobuf usage
u := &user.User{
Id: 1,
Name: "Alice",
Email: "alice@example.com",
}
// => Creates User message
// => Uses generated struct
data, err := proto.Marshal(u)
// => Serializes to binary format
// => data is []byte
// => Compact binary encoding
if err != nil {
panic(err)
}
fmt.Printf("Serialized %d bytes\n", len(data))
// => Output: Serialized 21 bytes
// Deserialize
u2 := &user.User{}
// => Empty User for deserialization
if err := proto.Unmarshal(data, u2); err != nil {
panic(err)
}
// => Deserializes from binary
fmt.Println(u2.GetName())
// => Output: Alice
// => Uses generated getter
}protobuf benefits:
- Type-safe schema-driven development
- Cross-language compatibility
- Efficient binary serialization
- Built-in versioning support
Production Tool: wire (Dependency Injection)
wire generates dependency injection code at compile time.
Installation:
go install github.com/google/wire/cmd/wire@latest
# => Installs wire toolDefine providers:
// File: wire.go
//go:build wireinject
// => Build tag: only compiled during wire generation
// => Not included in final binary
package main
import "github.com/google/wire"
//go:generate wire
// => Runs wire generator
func InitializeApp() (*App, error) {
// => Wire injector function (signature only)
// => Implementation generated by wire
wire.Build(
// => wire.Build lists providers
NewDatabase,
// => Provider function for *Database
NewRepository,
// => Provider function for *Repository
NewService,
// => Provider function for *Service
NewApp,
// => Provider function for *App
)
return nil, nil
// => Placeholder return (replaced by wire)
}Provider functions:
// File: providers.go
package main
import "database/sql"
func NewDatabase() (*sql.DB, error) {
// => Provider for database connection
return sql.Open("postgres", "...")
}
func NewRepository(db *sql.DB) *Repository {
// => Provider for repository
// => Takes *sql.DB as dependency
return &Repository{db: db}
}
func NewService(repo *Repository) *Service {
// => Provider for service
// => Takes *Repository as dependency
return &Service{repo: repo}
}
func NewApp(svc *Service) *App {
// => Provider for application
// => Takes *Service as dependency
return &App{svc: svc}
}Generated code (wire_gen.go):
// Code generated by Wire. DO NOT EDIT.
package main
func InitializeApp() (*App, error) {
// => Generated dependency wiring
// => Calls providers in correct order
db, err := NewDatabase()
// => Creates database first
if err != nil {
return nil, err
}
repository := NewRepository(db)
// => Creates repository with database
service := NewService(repository)
// => Creates service with repository
app := NewApp(service)
// => Creates app with service
return app, nil
// => Returns fully wired app
}wire benefits:
- Compile-time dependency injection (no reflection)
- Detects missing providers at generation time
- No runtime overhead
- Explicit dependency graph
Production Tool: sqlc (Type-Safe SQL)
sqlc generates type-safe Go code from SQL queries.
Installation:
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# => Installs sqlc toolConfiguration (sqlc.yaml):
version: "2"
sql:
- schema: "schema.sql"
queries: "queries.sql"
engine: "postgresql"
gen:
go:
package: "db"
out: "internal/db"Schema (schema.sql):
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);Queries (queries.sql):
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY name;
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING *;Generate code:
sqlc generate
# => Generates Go code from SQL
# => Creates internal/db/db.go and internal/db/querier.goGenerated code usage:
package main
import (
"context"
"database/sql"
"myproject/internal/db"
)
func main() {
conn, _ := sql.Open("postgres", "...")
queries := db.New(conn)
// => Creates query executor
ctx := context.Background()
// Type-safe query execution
user, err := queries.GetUser(ctx, 1)
// => GetUser generated from SQL
// => Returns db.User struct
users, err := queries.ListUsers(ctx)
// => ListUsers returns []db.User
newUser, err := queries.CreateUser(ctx, db.CreateUserParams{
Name: "Alice",
Email: "alice@example.com",
})
// => CreateUser with type-safe params
}sqlc benefits:
- Type-safe SQL queries
- Compile-time SQL validation
- No ORM overhead
- Database-agnostic
Summary
Go code generation ecosystem:
- go generate: Built-in directive for running generators
- stringer: Enum String() methods
- mockgen: Test mocks from interfaces
- protoc: Protocol Buffers (type-safe serialization)
- wire: Compile-time dependency injection
- sqlc: Type-safe SQL queries
Trade-offs:
| Approach | Pros | Cons |
|---|---|---|
| Manual code | No dependencies, full control | Verbose, error-prone |
| Code generation | Consistent, type-safe, eliminates boilerplate | Generated code, tool dependencies |
Progressive adoption:
- Start with stringer for enums (simplest, high value)
- Add mockgen for interface mocking (testing)
- Use protoc for cross-service communication
- Adopt wire for complex dependency graphs
- Consider sqlc for database-heavy applications
Best practices:
- Commit generated code to version control
- Run
go generate ./...in CI/CD - Add
// Code generated ... DO NOT EDITheaders - Document required tools in README