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:
- Always add the comment:
// Code generated by tool; DO NOT EDIT.— tools and reviewers recognize this - Commit generated files — reviewers can see what changed without running generators
- Run generators in CI and fail if output differs:
git diff --exit-code generated/ - Use separate directories for generated code:
internal/db/,internal/mock/,pb/ - 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:generatedirectives +go generate ./...run all generators in the package treestringergeneratesString()methods for enum types — add-trimprefixwhen constants share a prefixmockgengenerates test doubles from interfaces — usegomock.EXPECT()for call verificationsqlcgenerates type-safe Go functions from SQL schema + query files — eliminates runtime SQL errors- Custom generators:
text/templatefor configuration-driven output,go/astfor Go-source-driven output - Always add
// Code generated ...; DO NOT EDIT.header; commit generated files; validate in CI
Comments