The Problem

When building data-intensive applications in Go, you often find yourself writing repetitive boilerplate code. A common scenario is handling multiple data transformation layers:

  1. Converting GraphQL filters to database query parameters
  2. Converting database records to API response models
  3. Maintaining count queries with identical filter logic

This creates a maintenance nightmare. Every time you add a new field to your domain model, you must update three or more places in your codebase. The logic is identical — only the struct types differ.

Consider this pattern:

  • A filter conversion function that maps GraphQL filter inputs to database query parameters for list operations
  • An identical conversion function for count operations (same logic, different parameter struct)
  • A model conversion function that transforms database records to GraphQL response models

That’s 50% duplication before you even write real business logic.
The Traditional Solutions and Their Limitations

Option 1: Accept the Duplication

The simplest approach, but:

  • Maintenance becomes exponential as your domain grows
  • Bugs fixed in one place need fixing in multiple places
  • New developers don’t understand why the code is repeated
  • Refactoring becomes a multi-hour task

Option 2: Use Reflection

Reflection-based mappers like mapstructure or custom reflection utilities:

go
// Slow and loses type safetyresult, _ := mapstructure.Decode(source, &destination)

Drawbacks:

  • Runtime overhead (reflection is slow)
  • Loss of compile-time type checking
  • Harder to debug
  • Performance issues at scale
  • Complex error handling

Option 3: Generic Interfaces (Strategy Pattern)

Create an interface that multiple structs implement:

go
type FilterSetter interface {
    SetStringField(name string, value *string)
    SetIntField(name string, value *int)
}

Drawbacks:

  • Still requires massive switch statements in each implementation
  • No reduction in boilerplate — just reorganization
  • Difficult to add new field types without changing the interface
  • Still a lot of manual code to write and maintain

The Elegant Solution: go:generate

Go’s built-in code generation tool provides the perfect balance between maintainability, performance, and type safety. Instead of fighting duplication, you generate the repetitive code from a single source of truth.

How go:generate Works

Go includes a go:generate A command that runs arbitrary tools during the build process. The typical workflow:

  1. Define your data model in code (your source of truth)
  2. Write a generator program that reads this model
  3. Run go generate to create type-safe, optimized code
  4. The generated code is committed to version control (optional but recommended)

Why this is better:

  • Zero runtime overhead — generated code is optimized at compile time
  • Type-safe — full IDE support, compile-time checking
  • Single source of truth — maintain mapping in one place
  • Version control friendly — generated code is plain Go
  • Easy to review — generated code is human-readable
  • No magic — explicit, transparent transformation

A Real-World Implementation

Step 1: Define Your Mapping Configuration

Create a generator tool that contains your data transformation definitions:

go
type FieldMapping struct {
    FilterName
    string // Name in the filter input
    SqlcFieldOut string // Name in SQL parameter struct
    FieldType string // "string", "int64", "float64", "time"
    MinField string // For range queries
    MaxField string // For range queries
}
type ModelFieldMapping struct {
    DBField string // Name in database record
    ModelField string // Name in API response model
    FieldType
    string
    ConvertFunc string // Conversion
    function to apply
}

Step 2: List Your Fields Once

Instead of duplicating field mappings across multiple functions, list them once:

go
var fields = []FieldMapping{
    {
        FilterName: "Name", SqlcFieldOut: "Name", FieldType: "string"
    },
    {
        FilterName: "CreatedAt", SqlcFieldOut: "StartCreatedAt", FieldType: "time", MaxField:
        "EndCreatedAt"
    },
    //...etc
}
var modelFields = []ModelFieldMapping{
    {
        DBField: "Name", ModelField: "Name", ConvertFunc: "utils.NullStringToPointer"
    },
    {
        DBField: "CreatedAt", ModelField: "CreatedAt", ConvertFunc: "utils.NullTimeToTime"
    },
    //...etc
}

This is your single source of truth. All transformation logic derives from these definitions.

Step 3: Create a Template-Based Generator

Use Go’s text/template package to generate the boilerplate:

javascript
const template =
`// Code generated; DO NOT EDIT.package servicefunc (s *Service) GraphqlFilterToListParams(filter *model.Filter) sqlc.ListParams {    params := sqlc.ListParams{}        if filter != nil {{{- range .FilterFields}}{{- if eq .FieldType "string"}}        params.{{.SqlcFieldOut}} = utils.ToNullString(filter.{{.FilterName}}){{- else if eq .FieldType "time"}}        params.{{.SqlcFieldOut}} = utils.ToNullTime(filter.{{.FilterName}})        params.{{.MaxField}} = utils.ToNullTime(filter.{{.MaxField}}){{- end}}{{- end}}    }    return params}`

Step 4: Wire It Into Your Build

Add a go:generate directive to your service file:

go
package service//go:generate go run../../../cmd/gen/generator.go

Run the generator:

text
go generate ./...

This creates optimized, type-safe code automatically.

Benefits in Practice

Before: Maintaining 3 Functions

text
Service.go
├── graphqlFilterToListProductParams()     [200 lines]
├──
graphqlFilterToCountProductParams()    [195 lines - 95% duplicate]
└── dbGsmToGraphQLProduct()
[150 lines]

Every change requires updating multiple functions.

After: Maintaining 1 Configuration

text
Generator
└── fields = []FieldMapping{...}     [1 definition]
└── modelFields =
[]ModelFieldMapping{...}Generated:
└── filter_mapper.gen.go              [auto-generated from single
source]    
├── GraphqlFilterToListProductParams()    
├── GraphqlFilterToCountProductParams()    
└──
dbGsmToGraphQLProduct()

Add a field once, regenerate, done.

Maintenance Scenarios

Adding a new field:

  • Before: Edit 3+ functions, increase the chance of bugs
  • After: Add one line to your configuration, run go generate

Fixing a conversion bug:

  • Before: Fix in all places it appears
  • After: Fix in the generator template, regenerate

Changing field mapping rules:

  • Before: Manual refactor across multiple functions
  • After: Update the configuration, regenerate

Code review:

  • Before: Review duplicate logic in multiple places
  • After: Review the generator template once

Real-World Performance

The generated code is indistinguishable from handwritten code:

go
// Generated code - no reflection, pure assignmentfunc (s *Service) dbToGraphQL(db *sqlc.Record)
*model.Record {    return &model.Record{        Field1: utils.NullStringToPointer(db.Field1),
Field2: utils.NullTimeToTime(db.Field2),        // ... etc    }}

This compiles to identical machine code as handwritten code. Zero overhead.

Best Practices

1. Generate Only Repetitive Code

Use generators for boilerplate that follow predictable patterns. Don’t generate business logic.

2. Keep Generators Simple

Your generator tool should be ~100–200 lines. If it’s getting complex, you might be over-engineering.

3. Store Generated Files

Commit generated files to version control so:

  • Others can see what code is generated
  • CI can verify that the generation is up-to-date
  • Bisecting git history works correctly

4. Make Generation Part of CI/CD

Add a check to verify the generated code is current:

bash
# Generate codego generate ./...# Verify no files changed
git diff --exit-code

5. Document Your Mapping Configuration

Add comments to your field definitions explaining any non-obvious mappings:

go
var fields = []FieldMapping{
    // Database uses snake_case, GraphQL uses camelCase
    {
        FilterName: "StatusCode", SqlcFieldOut: "status_code",...
    },
}

Conclusion

Code generation isn’t new or exotic. Go’s go:generate makes it accessible for everyday problems. When you find yourself copying and pasting code patterns, consider:

  • Is this pattern repetitive?
  • Will it need to change frequently?
  • Does it follow a predictable structure?

If yes to all three, generate it.