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 pipelines

Environment 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() method

Generated 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:

ApproachProsCons
Manual String() methodsNo dependencies, full controlVerbose, error-prone, maintenance
stringerAutomatic, consistent, compile-time safeExternal 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/mock

Source 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 paths
go generate
# => Runs protoc via go:generate
# => Creates user.pb.go

Generated 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 tool

Define 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 tool

Configuration (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.go

Generated 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:

ApproachProsCons
Manual codeNo dependencies, full controlVerbose, error-prone
Code generationConsistent, type-safe, eliminates boilerplateGenerated code, tool dependencies

Progressive adoption:

  1. Start with stringer for enums (simplest, high value)
  2. Add mockgen for interface mocking (testing)
  3. Use protoc for cross-service communication
  4. Adopt wire for complex dependency graphs
  5. 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 EDIT headers
  • Document required tools in README
Last updated