Skip to main content
  1. Languages/
  2. Golang Guides/

Stop Writing Boilerplate: A Guide to Go Code Generation Tools

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

Introduction
#

In the landscape of modern software development in 2025, efficiency is paramount. While Go (Golang) is celebrated for its simplicity and readability, that philosophy often comes with a trade-off: boilerplate. Whether it’s implementing String() methods for enums, creating mock interfaces for testing, or mapping database rows to structs, writing repetitive code is tedious and error-prone.

Enter Code Generation.

Unlike languages that rely heavily on runtime reflection (looking at you, Java and Python) or complex macros (Rust), Go takes a pragmatic approach. It encourages generating static Go source code before compilation. This technique offers the best of both worlds: the expressiveness of metaprogramming with the performance and type safety of static typing.

In this quick guide, we will move beyond the basics. We’ll look at how to leverage the standard stringer tool and, more importantly, how to build your own lightweight code generator using Go’s text/template engine.

Prerequisites
#

To follow along, ensure you have the following setup:

  • Go Version: Go 1.23 or higher (we are assuming a standard 2025 environment).
  • IDE: VS Code (with Go extension) or JetBrains GoLand.
  • Environment: A clean directory for this project.

Initialize your project:

mkdir go-codegen-demo
cd go-codegen-demo
go mod init github.com/yourusername/go-codegen-demo

Understanding go:generate
#

The heart of Go’s code generation is the //go:generate directive. It is not a keyword; it is a specially formatted comment that the go generate command scans for.

When you run go generate ./..., Go scans your source files for these comments and executes the commands specified within them. It acts as a build-agnostic task runner specifically for code creation.

The Workflow
#

Here is how the code generation pipeline typically looks in a professional Go project:

flowchart TD A[Developer writes Source Code] --> B{Contains //go:generate?} B -- Yes --> C[Run 'go generate ./...'] C --> D[External Tool / Script executes] D --> E[Parses Code / Reads Config] E --> F[Generates .go file] F --> G[Developer runs 'go build'] B -- No --> G style A fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000 style C fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#000 style F fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000

Tool 1: The Classic stringer
#

Let’s start with a ubiquitous problem: Enums. Go doesn’t have a native enum type; we use const blocks. However, printing these constants usually results in an integer, which makes logs hard to read.

Instead of writing a massive switch statement manually, we use stringer.

Step 1: Install the Tool
#

go install golang.org/x/tools/cmd/stringer@latest

Step 2: Create the Enum
#

Create a file named payment.go. Note the comment at the top.

package main

import "fmt"

//go:generate stringer -type=PaymentStatus

type PaymentStatus int

const (
	Pending PaymentStatus = iota
	Authorized
	Captured
	Refunded
	Failed
)

func main() {
	status := Captured
	// Without stringer, this prints "2"
	// With stringer, this prints "Captured"
	fmt.Printf("Current Status: %s\n", status)
}

Step 3: Run Generation
#

Execute the following in your terminal:

go generate ./...

You will see a new file generated: paymentstatus_string.go. It contains an optimized approach (often using index tables) to map the integer to the string name.

Tool 2: Building a Custom Generator
#

While tools like stringer, jsonenums, or sqlc are fantastic, senior developers often face unique requirements. Perhaps you need to generate Reset() methods for a pool of objects, or specific validation logic based on struct tags.

Let’s build a simple generator that creates a Constructor function for a struct automatically.

Step 1: The Template Generator (gen/main.go)
#

Create a folder named gen and a file gen/main.go. This program will accept a type name and generate code for it.

// gen/main.go
package main

import (
	"flag"
	"html/template"
	"log"
	"os"
	"strings"
)

// Config holds data for the template
type Config struct {
	Package string
	Type    string
}

// simpleTemplate defines how our constructor looks
const simpleTemplate = `// Code generated by go-codegen-demo; DO NOT EDIT.
package {{.Package}}

// New{{.Type}} creates a new instance of {{.Type}}
func New{{.Type}}() *{{.Type}} {
	return &{{.Type}}{
		// Default initialization logic could go here
	}
}
`

func main() {
	typeName := flag.String("type", "", "The type name to generate a constructor for")
	pkgName := flag.String("pkg", "main", "The package name")
	flag.Parse()

	if *typeName == "" {
		log.Fatal("type argument is required")
	}

	// Prepare output filename: type_constructor.go
	filename := strings.ToLower(*typeName) + "_constructor.go"
	f, err := os.Create(filename)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// Execute template
	tmpl := template.Must(template.New("constructor").Parse(simpleTemplate))
	data := Config{
		Package: *pkgName,
		Type:    *typeName,
	}

	if err := tmpl.Execute(f, data); err != nil {
		log.Fatal(err)
	}
    
    log.Printf("Generated %s", filename)
}

Step 2: Using the Custom Generator
#

Now, let’s use this tool in our project. Go back to the root directory.

We need to build our tool first so go generate can call it. A common pattern is compiling it on the fly or ensuring it’s in the path. For this demo, we will run it directly using go run.

Create models.go:

package main

// We tell go generate to run the main.go inside the gen folder
// We pass the type "User" and package "main"

//go:generate go run ./gen/main.go -type=User -pkg=main

type User struct {
	ID       int
	Username string
	Email    string
}

Step 3: Generate and Verify
#

Run the generation command again:

go generate ./...

You should see output indicating Generated user_constructor.go. If you open that file, you will see your type-safe constructor ready to be used.

Comparison: Code Gen vs. Reflection
#

Why go through the trouble of writing generators when Go supports Reflection (reflect package)? This is a common question during code reviews.

Here is a breakdown of why Code Generation usually wins for production systems in 2025:

Feature Code Generation Runtime Reflection
Performance High. Code is compiled as standard Go code. No runtime overhead. Low. Reflection is significantly slower and prevents compiler optimizations (inlining).
Type Safety Checked at Compile Time. If types mismatch, the build fails. Checked at Runtime. Type errors cause panics in production.
Debuggability Easy. You can step through generated code in your debugger. Hard. Stepping into reflect library code is confusing and complex.
IDE Support Excellent. Autocomplete works perfectly on generated structs/methods. Poor. IDEs often cannot predict types manipulated via reflection.
Start-up Time Fast. Slower. Reflection often requires heavy initialization logic.

Best Practices and Pitfalls
#

As with any powerful tool, go generate can be misused. Here are tips to keep your project clean.

1. Commit Generated Code
#

There is a debate on whether to commit generated files to Git. In the Go community, the consensus is YES.

  • Why? It allows others to go get and build your project without needing your specific generator tools installed.
  • Exception: If the generated code is massive (megabytes) or platform-specific in a way that breaks cross-compilation.

2. Don’t Edit Generated Files
#

Generated files should be treated as read-only artifacts. Always add a header comment: // Code generated by tool; DO NOT EDIT. If you edit them manually, your changes will be wiped out the next time you run go generate.

3. Keep Generators Deterministic
#

Running go generate twice without changing the source should result in the exact same output file. Avoid putting timestamps in the generated file headers, as this creates unnecessary diffs in version control.

4. Use stringer, mockgen, and sqlc
#

Don’t reinvent the wheel.

  • Use stringer for enums.
  • Use uber-go/mock (formerly gomock) for testing mocks.
  • Use sqlc for generating type-safe database code from SQL queries.

Conclusion
#

Code generation is a superpower in the Go ecosystem. It allows us to maintain the language’s simplicity while automating the repetitive parts of software engineering. By mastering //go:generate and understanding how to construct simple AST or template-based generators, you elevate yourself from a user of the language to a tool builder.

In 2025, as systems become more complex, the ability to generate type-safe, performant boilerplate is what distinguishes a senior Go engineer from the rest.

Further Reading
#

Happy coding, and may your boilerplate be forever automated!