Generate All the Things

Go developers love code generation. Where C# uses reflection, attributes, and source generators, Go often uses tools that generate code before compilation.

This sounds backwards—writing code that writes code? But it has advantages: generated code is inspectable, debuggable, and has zero runtime overhead.

The go generate Command

Add a magic comment to your source file:

//go:generate stringer -type=Status

type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusCompleted
)

Run:

go generate ./...

This runs stringer -type=Status in the package directory, which generates status_string.go:

// Code generated by "stringer -type=Status"; DO NOT EDIT.

func (i Status) String() string {
    switch i {
    case StatusPending:
        return "StatusPending"
    case StatusActive:
        return "StatusActive"
    case StatusCompleted:
        return "StatusCompleted"
    }
    return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}

The generated code is committed to your repo. It’s just Go code.

Common Code Generators

stringer

Generates String() methods for constants:

//go:generate stringer -type=LogLevel

We covered this in the enums post.

mockgen

Generates mock implementations:

//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repo

Creates mock structs for testing.

sqlc

Generates type-safe database code from SQL:

-- queries/users.sql
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;

-- name: ListUsers :many
SELECT id, name, email FROM users ORDER BY name;
//go:generate sqlc generate

Produces:

func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
    row := q.db.QueryRowContext(ctx, getUserSQL, id)
    var i User
    err := row.Scan(&i.ID, &i.Name, &i.Email)
    return i, err
}

Type-safe queries with no reflection.

protoc (Protocol Buffers)

Generates Go code from .proto files:

//go:generate protoc --go_out=. --go-grpc_out=. api.proto

oapi-codegen

Generates code from OpenAPI specs:

//go:generate oapi-codegen -package api -generate types,server openapi.yaml

ent

Generates ORM code:

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema

Writing Your Own Generator

Generators are just programs. You can write your own:

// gen/main.go
package main

import (
    "os"
    "text/template"
)

var tmpl = `// Code generated; DO NOT EDIT.
package {{.Package}}

var Version = "{{.Version}}"
var BuildTime = "{{.BuildTime}}"
`

func main() {
    t := template.Must(template.New("version").Parse(tmpl))
    
    data := map[string]string{
        "Package":   os.Getenv("GOPACKAGE"),
        "Version":   os.Getenv("VERSION"),
        "BuildTime": time.Now().Format(time.RFC3339),
    }
    
    f, _ := os.Create("version_gen.go")
    defer f.Close()
    t.Execute(f, data)
}

Use it:

//go:generate go run gen/main.go

The go:generate Directive

The comment format:

//go:generate command arg1 arg2

Rules:

  • Must start with //go:generate (no space after //)
  • The command runs in the package directory
  • Environment variables available: $GOPACKAGE, $GOFILE, $GOLINE
  • Output to stderr shows during generation

Multiple directives are fine:

//go:generate stringer -type=Status
//go:generate mockgen -source=service.go -destination=mock_service.go

Comparing to C# Source Generators

C# has source generators (Roslyn) that run during compilation:

[Generator]
public class MyGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // Generate code based on syntax trees
    }
}
AspectC# Source GeneratorsGo go:generate
When runsDuring compilationBefore compilation
OutputIn-memoryFiles in repo
DebuggingHarderEasy (it’s just files)
IDE supportIntegratedManual regeneration
DependenciesPart of buildExternal tools
IncrementalYesManual

C#’s generators are more integrated. Go’s are more transparent.

Best Practices

1. Commit generated code

Generated code should be in your repo:

  • CI doesn’t need generator tools
  • Code review includes generated changes
  • git blame works

2. Mark generated files

Start with the standard header:

// Code generated by TOOL; DO NOT EDIT.

Linters and editors recognize this and treat the file specially.

3. Regenerate in CI

Verify generated code is up-to-date:

- name: Check generated code
  run: |
    go generate ./...
    git diff --exit-code

Fails if generated code differs from committed code.

4. Document generation

In your README or Makefile:

generate:
	go generate ./...

.PHONY: generate

5. Keep generators fast

go generate runs serially. Slow generators hurt iteration speed.

Real-World Example

A typical project might have:

// internal/domain/status.go
//go:generate stringer -type=Status

// internal/repository/repository.go  
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repository

// internal/database/queries.sql
//go:generate sqlc generate

// api/openapi.yaml
//go:generate oapi-codegen -package api -generate types,server,spec -o api_gen.go openapi.yaml

Run go generate ./... and all these tools produce code.

The Honest Take

Code generation is more prominent in Go than C#. It’s a cultural thing—Go prefers explicit, inspectable code over runtime magic.

What Go does well:

  • Generated code is transparent
  • Easy to debug (it’s just files)
  • No runtime reflection overhead
  • Tools are standalone programs

What C# does better:

  • Source generators are integrated
  • IDE understands generated code immediately
  • Incremental generation
  • No manual regeneration step

The verdict: If you’re not used to code generation, it feels weird. But once you embrace it, you appreciate the transparency.

sqlc in particular is wonderful—type-safe database access without ORM magic, because the queries are analyzed at generation time.

Learn to love go generate ./.... It’s part of the Go workflow.


Next up: debugging with Delve—getting productive with dlv after Visual Studio’s polished debugging experience.