Skip to main content

Code Generation in Go

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 5 min read

Code generation in Go reduces boilerplate by automating repetitive patterns: String() methods for enums, mocks for interfaces, type-safe SQL query wrappers, protobuf Go stubs. The go generate mechanism runs these generators as part of the build workflow, keeping generated code in sync with source.

The principle: generate code that you’d otherwise write by hand, commit the output, and re-run the generator only when the source changes.

The go:generate Directive

A //go:generate comment in any .go file specifies a command to run. go generate ./... runs all generators in the package tree:

// In the file that defines the type:
//go:generate stringer -type=Status
//go:generate mockgen -source=service.go -destination=mock_service.go

type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusClosed
)
go generate ./...    # runs all generators
go generate ./pkg/... # specific subtree

Generators are just programs — anything you can run in a shell. The directive captures the command to run, and go generate runs it.

stringer: String() for Enums

The most common generator. stringer reads your const block and generates a String() method that returns the constant name:

go install golang.org/x/tools/cmd/stringer@latest
//go:generate stringer -type=Direction

type Direction int

const (
    North Direction = iota
    South
    East
    West
)

After go generate, a direction_string.go file is created:

// Code generated by "stringer -type=Direction"; DO NOT EDIT.
func (i Direction) String() string {
    switch i {
    case North: return "North"
    case South: return "South"
    case East:  return "East"
    case West:  return "West"
    default:    return fmt.Sprintf("Direction(%d)", int(i))
    }
}

Now fmt.Println(North) prints North instead of 0. Every log line, error message, and debug output automatically uses the name.

Options:

  • -linecomment: use the line comment as the string instead of the constant name
  • -trimprefix=Prefix: strip a common prefix from all names
  • -output=filename.go: control the output filename

mockgen: Test Doubles from Interfaces

mockgen generates mock implementations from interface definitions:

go install go.uber.org/mock/mockgen@latest
// service.go
//go:generate mockgen -source=service.go -destination=mock/mock_service.go -package=mock

type UserService interface {
    GetUser(ctx context.Context, id string) (*User, error)
    CreateUser(ctx context.Context, req CreateUserRequest) (*User, error)
    DeleteUser(ctx context.Context, id string) error
}

After generating, use in tests:

import "myapp/mock"
import "go.uber.org/mock/gomock"

func TestHandler(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockSvc := mock.NewMockUserService(ctrl)

    // Expect GetUser to be called with "u1" and return Alice
    mockSvc.EXPECT().
        GetUser(gomock.Any(), "u1").
        Return(&User{ID: "u1", Name: "Alice"}, nil)

    // Test handler using mockSvc
    handler := NewUserHandler(mockSvc)
    req := httptest.NewRequest("GET", "/users/u1", nil)
    rec := httptest.NewRecorder()
    handler.ServeHTTP(rec, req)

    assert.Equal(t, 200, rec.Code)
}

gomock verifies that the mock was called with the expected arguments and the expected number of times — test failures tell you exactly what was called vs expected.

sqlc: Type-Safe SQL from Schema

sqlc reads SQL schema files and query files, then generates type-safe Go functions. No more string queries and manual rows.Scan:

go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# sqlc.yaml
version: "2"
sql:
  - engine: "postgresql"
    queries: "queries.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "internal/db"
-- schema.sql
CREATE TABLE users (
    id      TEXT PRIMARY KEY,
    name    TEXT NOT NULL,
    email   TEXT NOT NULL UNIQUE
);

-- 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 (id, name, email) VALUES ($1, $2, $3) RETURNING *;

After sqlc generate, you get:

// internal/db/query.sql.go — generated, don't edit

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

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { ... }

Usage:

queries := db.New(pool)
user, err := queries.GetUser(ctx, "u1")  // type-safe, no string building

sqlc eliminates a class of runtime errors (wrong column types, missing Scan fields) by catching them at generation time.

Writing a Custom Generator

When existing tools don’t fit, write your own. The minimal pattern: read a source file, transform it, write Go output:

// cmd/gen-routes/main.go
// Generates a routes_gen.go file from a routes.yaml config

package main

import (
    "os"
    "text/template"
    "gopkg.in/yaml.v3"
)

type Route struct {
    Method  string `yaml:"method"`
    Path    string `yaml:"path"`
    Handler string `yaml:"handler"`
}

var routesTmpl = template.Must(template.New("routes").Parse(`
// Code generated by gen-routes; DO NOT EDIT.
package {{.Package}}

import "net/http"

func RegisterRoutes(mux *http.ServeMux) {
{{range .Routes}}
    mux.HandleFunc("{{.Method}} {{.Path}}", {{.Handler}})
{{end}}
}
`))

func main() {
    data, err := os.ReadFile("routes.yaml")
    if err != nil { log.Fatal(err) }

    var routes []Route
    yaml.Unmarshal(data, &routes)

    out, err := os.Create("routes_gen.go")
    if err != nil { log.Fatal(err) }
    defer out.Close()

    routesTmpl.Execute(out, map[string]any{
        "Package": "main",
        "Routes":  routes,
    })
}

Add a directive in your package:

//go:generate go run ./cmd/gen-routes

After writing generated files, always run gofmt or use format.Source from go/format to ensure consistent formatting.

AST-Based Generators

For generators that read Go source (not YAML/SQL), use go/parser and go/ast to analyze types and generate code:

import (
    "go/ast"
    "go/parser"
    "go/token"
)

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "types.go", nil, parser.ParseComments)

// Find all structs with a specific comment marker
ast.Inspect(file, func(n ast.Node) bool {
    ts, ok := n.(*ast.TypeSpec)
    if !ok { return true }

    st, ok := ts.Type.(*ast.StructType)
    if !ok { return true }

    // Generate code for this struct
    generateForStruct(ts.Name.Name, st)
    return true
})

This is how stringer, mockgen, and protoc-gen-go work internally.

Maintaining Generated Code

Best practices for generated files:

  1. Always add the comment: // Code generated by tool; DO NOT EDIT. — tools and reviewers recognize this
  2. Commit generated files — reviewers can see what changed without running generators
  3. Run generators in CI and fail if output differs: git diff --exit-code generated/
  4. Use separate directories for generated code: internal/db/, internal/mock/, pb/
  5. Never edit generated files — changes are lost on next go generate
# Makefile
generate:
	go generate ./...
	gofmt -w .

check-generated: generate
	git diff --exit-code  # fail if generators changed any files

Summary

  • //go:generate directives + go generate ./... run all generators in the package tree
  • stringer generates String() methods for enum types — add -trimprefix when constants share a prefix
  • mockgen generates test doubles from interfaces — use gomock.EXPECT() for call verification
  • sqlc generates type-safe Go functions from SQL schema + query files — eliminates runtime SQL errors
  • Custom generators: text/template for configuration-driven output, go/ast for Go-source-driven output
  • Always add // Code generated ...; DO NOT EDIT. header; commit generated files; validate in CI

Resources

Comments

👍 Was this article helpful?