← Plugin catalog
Developer Tools

Gophers

Murat Mirgün Ercan v0.1.0

Publisher description

From the marketplace listing

Get focused guidance for writing, reviewing, testing, and improving Go code. Use 26 skills covering APIs, concurrency, databases, error handling, observability, performance, documentation, and idiomatic design.

Language: English · Automatically detected from descriptions.

Publisher keywords

Search terms declared by the publisher.

Matches for “testing”

Exact text from the indicated source. A mention alone does not establish support for your task.

Publisher keywords · listing

go golang skills code-review testing concurrency observability

Publisher full description

Get focused guidance for writing, reviewing, testing, and improving Go code. Use 26 skills covering APIs, concurrency, databases, error handling, observability, performance, documentation, and idiomatic design.

Files & skills

File archives

Plugin package142 files · 189 KBBrowse files →
Skill instructions
go-clean-architecture9.45 KB

View saved version →

---
name: go-clean-architecture
description: "Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase, Repository, Delivery layers, inward dependency rule, 'framework/database is a detail'. Apply when untangling a monolith or checking whether business logic is testable without HTTP or DB."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+. Framework-agnostic: works with Gin, Echo, Fiber, Chi, or net/http; swap freely."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Clean Architecture

A Go service organized into four concentric layers — Domain, Usecase, Repository, Delivery — where source code depends *inward only*. Done well, the HTTP framework and the database are interchangeable details; the business logic is testable without either.

This skill is framework-agnostic. Swap Gin for Fiber, Echo, Chi, or `net/http` by replacing the delivery layer — zero changes elsewhere.

## Core Rules

1. **Dependency Rule.** Source depends inward: Delivery → Usecase → Domain. Repository implements interfaces declared in Domain. Domain depends on nothing.
2. **Framework is a detail.** Gin/Fiber/Echo/Chi/net-http types live only in `internal/delivery/`. Usecases see plain Go values.
3. **Database is a detail.** SQL, sqlx, sqlc, pgx, GORM live only in `internal/repository/`. Usecases see repository interfaces.
4. **Domain owns the interfaces; layers below provide implementations.** `UserRepository` is an interface in `internal/domain`; the Postgres struct is in `internal/repository` and unexported.
5. **DTOs at the edges.** Delivery layer maps HTTP request bodies to domain inputs and domain entities to response bodies. Usecases never see `*gin.Context`, `http.Request`, or DB rows.
6. **`cmd/<binary>/main.go` is the only place that knows the whole system.** Wiring (DI) is explicit, framework-free Go code.

## When This Pays Off

| Symptom | What clean architecture buys you |
|---|---|
| HTTP handlers contain SQL | Move SQL into a repository; handlers shrink to 5 lines |
| Tests need a running DB | Mock the repository interface; usecase tests run in milliseconds |
| Swapping web frameworks is a rewrite | Replace `internal/delivery/http`; nothing else touched |
| Business rules duplicated across handlers | Single usecase function, called by HTTP, gRPC, and a CLI |
| ORM hooks fire in surprising places | Repository methods are explicit; no hidden behavior |

If the service is a 200-line cron job, this skill is overkill. If it will live 3+ years and grow features, it's the cheapest insurance you can buy.

## Project Structure

```
myapp/
  cmd/
    api/main.go             # entry point: config → DI → start server
    worker/main.go          # different entry, same Domain & Usecase
  internal/
    domain/                 # entities, value objects, repository INTERFACES, domain errors
      user.go
      order.go
      errors.go
    usecase/                # business logic; depends only on domain
      user_usecase.go
      order_usecase.go
    repository/             # implementations of domain interfaces (Postgres, in-memory, ...)
      user_postgres.go
      order_postgres.go
    delivery/               # framework-specific adapters
      http/                 # Gin/Echo/Chi/net-http handlers and routes
        user_handler.go
        order_handler.go
      grpc/                 # gRPC server adapters (if applicable)
  pkg/                      # exported, importable from outside (if you publish a library)
  migrations/               # SQL migrations
  config/
  go.mod
```

> Read [references/domain.md](references/domain.md), [references/usecase.md](references/usecase.md), [references/repository.md](references/repository.md), and [references/delivery.md](references/delivery.md) for the per-layer responsibilities.

## The Four Layers

| Layer | Package | Can import | Must not import |
|---|---|---|---|
| Domain | `internal/domain` | stdlib only | usecase, repository, delivery, frameworks |
| Usecase | `internal/usecase` | domain | repository (concrete), delivery, frameworks |
| Repository | `internal/repository` | domain, DB driver | delivery, frameworks |
| Delivery | `internal/delivery/...` | domain, usecase (via interface), framework | repository (concrete) |

A `golangci-lint` config with `depguard` enforces these rules at CI time.

## Layer Sketches

```go
// Domain — pure interfaces and entities, no I/O.
package domain
type User struct { ID, Email, Name string; CreatedAt time.Time }
type UserRepository interface {
    Get(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, u *User) error
}
type UserService interface {
    Create(ctx context.Context, in CreateUserInput) (*User, error)
}
```

```go
// Usecase — business logic, depends only on domain interfaces.
type userUsecase struct{ repo domain.UserRepository }

func NewUserUsecase(repo domain.UserRepository) domain.UserService {
    return &userUsecase{repo: repo}
}
```

```go
// Repository — concrete adapter, translates driver errors to domain errors.
type postgresUserRepo struct{ db *sql.DB }
func NewUserRepository(db *sql.DB) domain.UserRepository { return &postgresUserRepo{db: db} }
```

```go
// Delivery — HTTP framework lives only here; swap freely.
type UserHandler struct{ svc domain.UserService }
func NewUserHandler(svc domain.UserService) *UserHandler { return &UserHandler{svc: svc} }
```

> Read [references/domain.md](references/domain.md), [references/usecase.md](references/usecase.md), [references/repository.md](references/repository.md), and [references/delivery.md](references/delivery.md) for full code examples per layer.

## Wiring in `main.go`

```go
// cmd/api/main.go — the only place that knows the whole system.
db, _ := sql.Open("postgres", cfg.DBURL)
userRepo := repository.NewUserRepository(db)
userSvc  := usecase.NewUserUsecase(userRepo)
userH    := delivery.NewUserHandler(userSvc)
r := gin.New()
r.POST("/api/v1/users", userH.Create)
_ = r.Run(cfg.Addr)
```

This is the only file that imports every internal package. Adding a feature touches each layer plus one DI line here — predictable.

> Read [references/anti-patterns.md](references/anti-patterns.md) for the failure modes — leaking `*gin.Context` into usecases, importing repository from delivery, returning concrete types instead of interfaces.

## Error Flow

```
Repository                Usecase                  Delivery
sql.ErrNoRows         →   domain.ErrNotFound   →   404
unique violation      →   domain.ErrConflict   →   409
validation rule       →   domain.ErrValidation →   422
unknown               →   wrapped error        →   500 (logged)
```

Map domain errors to HTTP status codes in the delivery layer — never in the domain. The mapping changes per transport (HTTP 404 ↔ gRPC NotFound).

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `*gin.Context` parameter in a usecase | Locks the system into Gin forever | Pass `context.Context` and plain inputs |
| Repository returns `*sql.Rows` | Usecase has to know about `database/sql` | Return domain entities only |
| Concrete `*userUsecase` exported | Direct instantiation bypasses constructor (and the dependency rule) | Return `domain.UserService` from `New...` |
| Delivery imports repository directly | Skips the usecase; logic moves to handlers | Inject `domain.UserService`, not `*postgresUserRepo` |
| Same struct for DTO and Domain entity | Adding HTTP-only fields pollutes the domain | Separate request/response structs in delivery |
| Domain importing `errors.Is(err, gorm.ErrRecordNotFound)` | Couples domain to GORM | Translate driver errors in repository to `domain.ErrXxx` |
| Wiring scattered across init() funcs | Implicit order, hard to debug | All DI in `main.go`, top-to-bottom |

## Verification Checklist

Each item maps to a command you can run; the expected outcome is in parentheses.

- [ ] `go list -deps ./internal/domain | grep -v '^\(internal/\|<modpath>\)' | grep -v '^[a-z]*$'` shows only stdlib paths (domain has no third-party deps)
- [ ] `go list -f '{{.Imports}}' ./internal/usecase/... | tr ' ' '\n' | grep -E '(gin|echo|fiber|chi|database/sql|gorm|pgx)'` is empty (usecase touches no framework/driver)
- [ ] `go list -f '{{.Imports}}' ./internal/delivery/... | tr ' ' '\n' | grep 'internal/repository'` is empty (delivery never imports repository)
- [ ] `grep -rn 'func New[A-Z]' internal/usecase | grep -v 'domain\.\|interface'` is empty (every `NewX` returns a domain interface, not a concrete type)
- [ ] `grep -rln 'internal/repository' cmd/ internal/` lists only `cmd/*/main.go` (main is the only wiring site)
- [ ] `go test ./internal/usecase/... -count=1` passes with no DB available (usecase mocks the repository interface)
- [ ] Swapping HTTP framework: `git mv internal/delivery/http internal/delivery/http_old && go build ./internal/usecase/... ./internal/repository/... ./internal/domain/...` succeeds — only delivery is dirty

## References

- [references/domain.md](references/domain.md) — entities, value objects, interface ownership, sentinel errors
- [references/usecase.md](references/usecase.md) — orchestration patterns, input DTOs, testing
- [references/repository.md](references/repository.md) — concrete adapters, error translation, transactions
- [references/delivery.md](references/delivery.md) — HTTP handlers, framework swap, error → status mapping
- [references/anti-patterns.md](references/anti-patterns.md) — leaks across layer boundaries

Referenced files: 6

go-code-review8.69 KB

View saved version →

---
name: go-code-review
description: "Invoke this skill to systematically review a Go change against community style standards before merging. Walks the diff topic by topic — formatting, errors, naming, concurrency, interfaces, data structures, security, declarations, functions, style, logging, imports, generics, testing — flagging issues with line references and severity (must-fix / should-fix / nit). Apply proactively before any Go PR ships."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Examples assume Go 1.21+ for log/slog references."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Code Review

A repeatable, opinionated review pass for Go code. Read the diff file by file, walk each topic in order, flag findings with file:line references, then group by severity.

## Core Rules

1. **Mechanical checks first.** Never start a human review until `gofmt`, `go vet`, and `golangci-lint` are clean. They free your attention for what tools cannot catch.
2. **One file at a time, topic by topic.** Walk the diff in order and apply each topic checklist below. Switching topics mid-file loses the thread.
3. **Every finding cites a rule.** `file:line` plus the rule name (`go-naming: initialisms`) — never a bare opinion.
4. **Severity is non-negotiable.** Must-Fix (correctness/security/data-loss/broken contract) blocks merge; Should-Fix (significant design or style issue); Nit (small preference, flag once).
5. **Drop what you cannot defend.** After flagging, re-read and remove any finding you would not stand behind in a thread.
6. **Praise non-trivial improvements.** A review without acknowledgement teaches only avoidance.

## Review Procedure

1. Run mechanical checks: `gofmt -d ./...`, `go vet ./...`, `golangci-lint run ./...`, `go test ./... -race -short`.
2. Read the diff one file at a time. For each file, walk the topic checklists below in order.
3. Flag every issue with `file:line` and the rule name that justifies it.
4. After all files are reviewed, re-read flagged items and drop any you cannot justify.
5. Group findings by **Must Fix / Should Fix / Nit** using the rubric below.

> Use [references/review-template.md](references/review-template.md) when writing up the review for consistent severity grouping and tone.

## Automated Checks

```bash
gofmt -l ./... && go vet ./... && golangci-lint run ./... && go test ./... -race -short
```

Fix anything the tools find before continuing. See [go-linting](../go-linting/SKILL.md) for setup.

## Formatting

- [ ] `gofmt`/`goimports` clean; long lines break by semantics, not column count

## Documentation

- [ ] Exported symbols documented (starts with name, ends with `.`); package comment adjacent to `package` clause; non-trivial unexported have intent comments; named returns only when they clarify

See [go-documentation](../go-documentation/SKILL.md).

## Error Handling

- [ ] No discarded errors (`_ = f()`) without a written justification
- [ ] Error strings are lowercase with no trailing punctuation
- [ ] Errors wrap with `%w` when the caller may want to inspect; `%v` only when hiding is deliberate
- [ ] No in-band magic values (`-1`, `""`, `nil`) for failure — multi-return with an `error` or `ok bool`
- [ ] Error path comes first; the success path stays unindented
- [ ] Each error is handled exactly once (log **or** return, not both)

See [go-error-handling](../go-error-handling/SKILL.md).

## Naming

- [ ] `MixedCaps` / `mixedCaps` only — no underscores or `SCREAMING_SNAKE`
- [ ] Initialisms are uniformly cased: `URL`, `ID`, `HTTP`, `XMLHTTPRequest`, `serveHTTP`
- [ ] Short names for short scopes (`i`, `r`, `ctx`); longer names for wider scopes
- [ ] Receiver names are one or two letters, consistent across methods; no `this`/`self`/`me`
- [ ] Packages don't stutter (`http.Server`, not `http.HTTPServer`)
- [ ] Package names avoid `util`, `helpers`, `common`, `misc`
- [ ] No identifiers shadow builtins (`len`, `error`, `cap`, `new`, `make`, `copy`, `any`)

See [go-naming](../go-naming/SKILL.md) and [go-packages](../go-packages/SKILL.md).

## Declarations

- [ ] Related `var`/`const`/`type` are in grouped blocks; unrelated kept separate
- [ ] `var` for intentional zero values; `:=` for computed locals
- [ ] Variables scoped as narrowly as is readable (if-init where it fits)
- [ ] Struct literals use field names; zero-value fields are omitted
- [ ] Enums start at `iota + 1` (or zero is explicitly meaningful)
- [ ] `any` instead of `interface{}` in new code

See [go-declarations](../go-declarations/SKILL.md).

## Control Flow

- [ ] No `else` after a returning/breaking/continuing `if`; no `:=` shadowing of outer `ctx`/`err`; map iteration is order-agnostic; labeled `break`/`continue` for switch-in-loop

See [go-control-flow](../go-control-flow/SKILL.md).

## Functions

- [ ] File order: type → constructor → exported → unexported → utilities; wrapped signatures one-per-line; no pointer-to-interface; bool/int params renamed via type or commented; printf-style helpers end in `f`

See [go-functions](../go-functions/SKILL.md).

## Interfaces

- [ ] Defined in the consumer package; not "just for mocking" on the implementor; consistent receivers per type; compile-time `var _ I = (*T)(nil)` on exported implementations

See [go-interfaces](../go-interfaces/SKILL.md).

## Concurrency

- [ ] Goroutine lifetimes clear (bounded by `ctx.Done()` or documented); APIs synchronous by default; `context.Context` first param, never struct field; lock order documented; sender closes channels

Example to flag (`pkg/worker/worker.go:42`):

```go
go s.process(req)   // ✗ no ctx, no done signal — leak on shutdown
```

vs. acceptable:

```go
s.wg.Add(1)
go func() { defer s.wg.Done(); s.process(ctx, req) }()
```

See [go-concurrency](../go-concurrency/SKILL.md).

## Data Structures

- [ ] `var t []T` for nil slices, `[]T{}` only when empty non-nil is required (JSON output); copies of structs containing `sync.Mutex` flagged; slice/map at API boundaries copied or borrowing documented

See [go-data-structures](../go-data-structures/SKILL.md).

## Security & Logging

- [ ] `crypto/rand` for secret material (never `math/rand`); no library `panic` for ordinary failures
- [ ] `log/slog` (not `log`/`fmt.Println`); static message + structured attrs; secrets and PII never logged

See [go-defensive](../go-defensive/SKILL.md) and [go-logging](../go-logging/SKILL.md).

## Imports

- [ ] Grouped: stdlib → external → local; no rename unless collision; no blank import outside `main`/tests; no dot imports

See [go-packages](../go-packages/SKILL.md).

## Generics

- [ ] Justified by ≥2 real call sites; constraint is the loosest that compiles; no generics-just-for-an-interface

See [go-generics](../go-generics/SKILL.md).

## Testing

- [ ] Tests cover the new behavior at the right level; failure messages include what/inputs/got/want
- [ ] `httptest.NewServer` over hand-rolled mocks; `TestMain` only when truly necessary; `Example*` for non-trivial APIs

> Read [references/integrative-example.md](references/integrative-example.md) to see the rules applied together on a small HTTP server.
>
> Read [references/severity-rubric.md](references/severity-rubric.md) when deciding whether to label a finding must-fix, should-fix, or nit.

## Quick Severity Rubric

| Severity | Examples |
|---|---|
| Must Fix | Race, security bug, swallowed error, broken API, data loss |
| Should Fix | Wrong layer for an interface, panic in a library, leaky goroutine |
| Nit | Name preference, comment phrasing, ordering within a block |

## Anti-Patterns

These are reviewer anti-patterns — bad habits to avoid when *writing* the review itself:

| Anti-pattern | Do this instead |
|---|---|
| "I would have written this differently" | drop it, or cite a concrete rule |
| Listing every `nit` you noticed | flag once, note "× N similar" |
| No severity labels; comment without `file:line` | label Must / Should / Nit; anchor with `path:line` |
| Reviewing the author, not the code | write about the change, not the person |
| Re-doing the work in the review | point to the rule and let them write it |
| Approving without reading tests | tests are part of the diff |

## Verification Checklist

- [ ] Every finding has `file:line` + a rule citation; no bare opinions
- [ ] No duplicates (note "× N similar" if widespread); severity matches the rubric
- [ ] Tests were read, not just counted
- [ ] Praise included for non-trivial improvements; tone is about the change, never the author

## References

- [review-template.md](references/review-template.md) — markdown shape for posting a review
- [severity-rubric.md](references/severity-rubric.md) — extended must/should/nit guidance
- [integrative-example.md](references/integrative-example.md) — sample HTTP server walked through

Referenced files: 4

go-code-style7.14 KB

View saved version →

---
name: go-code-style
description: "Use when writing or reviewing Go code for clarity, formatting, control flow, variable declarations, switch usage, and function design. Covers the priority order (clarity > simplicity > concision > maintainability > consistency), gofmt rules, early-return / no-else patterns, switch over if-else chains, slice/map initialization, and value-vs-pointer choices. Apply proactively to every Go change, even when the user has not asked about style."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Works on any Go version supported by gofmt; range-over-int requires Go 1.22+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Code Style

`gofmt` handles the mechanical layout. This skill covers the decisions a formatter cannot make: where to break complexity, when to invert an `if`, when a `switch` beats an `else if` chain, and how to pick value vs pointer arguments. The rule of thumb is the Go Proverb: **clear is better than clever**.

## Core Rules

1. **Run `gofmt`/`goimports`.** No exceptions, no debates.
2. **Handle errors and edge cases first**, return early, keep the happy path at minimum indentation.
3. **Eliminate unnecessary `else`** when the `if` body ends in `return`/`break`/`continue`.
4. **Prefer `switch` over `if`/`else if` chains** that compare the same value.
5. **Initialize slices and maps explicitly**, never let a nil map reach a write.
6. **Pass small values; pass pointers when mutating or when the type is large (~128B+).**

## Style Priority Order

Apply in this order when two principles collide.

| Priority | Question | Beats |
|---|---|---|
| 1. Clarity | Can a reader understand intent without extra context? | everything else |
| 2. Simplicity | Is this the simplest expression of the idea? | concision, consistency |
| 3. Concision | Does every line earn its place? | consistency |
| 4. Maintainability | Will this be safe to modify? | consistency |
| 5. Consistency | Does it match nearby code? | — |

A "consistent" piece of bad code is still bad code. Clarity wins.

> Read [references/formatting-and-layout.md](references/formatting-and-layout.md) for line-breaking, multi-line signatures, file/declaration order.

## Control Flow

### Early return: keep the happy path flat

```go
func process(data []byte) (*Result, error) {
    if len(data) == 0 {
        return nil, errors.New("empty data")
    }
    parsed, err := parse(data)
    if err != nil {
        return nil, fmt.Errorf("parsing: %w", err)
    }
    return transform(parsed), nil
}
```

### No unnecessary `else`

When the `if` body unconditionally exits (`return`/`break`/`continue`), drop the `else`. For assignments, prefer default-then-override:

```go
// Good
lvl := slog.LevelInfo
if debug {
    lvl = slog.LevelDebug
}
```

### Switch over if/else chains

When all branches compare the same value, `switch` makes intent explicit and `exhaustive` can verify completeness:

```go
switch status {
case StatusActive:
    activate()
case StatusInactive:
    deactivate()
case StatusPaused:
    pause()
default:
    panic(fmt.Sprintf("unexpected status: %d", status))
}
```

A tagless `switch` replaces a chain of unrelated `if/else if` conditions — first matching case wins.

### Extract complex conditions

When an `if` has 3+ operands, hoist into named booleans so the names document the business rule:

```go
isAdmin := user.Role == RoleAdmin
isOwner := resource.OwnerID == user.ID
if isAdmin || isOwner || permissions.Has(PermOverride) {
    allow()
}
```

> Read [references/control-flow.md](references/control-flow.md) for tagless switch, guard clauses, and labelled break/continue.

## Variable Declarations

Use `:=` for non-zero initializers, `var` when the zero value is the start.

```go
var count int          // start at 0
name := "default"      // non-zero
var buf bytes.Buffer   // zero value is ready to use
```

### Initialize slices and maps explicitly

A nil map panics on write; a nil slice JSON-encodes to `null` (a UX surprise for API consumers).

```go
users := []User{}                       // explicit empty
m := map[string]int{}                   // explicit empty
users = make([]User, 0, len(ids))       // preallocate when size is known
m = make(map[string]int, len(items))    // preallocate map buckets
```

Do not speculatively preallocate large capacities — `make([]T, 0, 1000)` wastes memory when the common case is 10.

### Composite literals: name the fields

```go
srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
```

Positional fields break the moment the type adds or reorders a field.

## Function Design and Argument Passing

- **Short and focused**, ≤ 4 parameters. Beyond that, use an options struct or functional options.
- **Parameter order:** `ctx context.Context` first, then inputs, then output destinations.
- **`range` over index loops** (`range n` since Go 1.22).
- **Pass small values, pointers for mutation / large structs (~128B+) / meaningful `nil`.** `*string`, `*int` parameters add indirection with no real saving. `sync.Mutex` and types embedding one are never copied — `go vet copylocks` catches it.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Deeply nested `if` chains | Happy path scrolls off-screen | Invert, return early |
| `if ok { return a } else { return b }` | `else` is dead weight | Drop the `else` |
| `if x == A else if x == B else if x == C` | Reader has to verify all comparisons share `x` | `switch x { ... }` |
| Positional struct literal | Breaks silently on field reorder | Named fields |
| Nil map write | Runtime panic | `make(...)` or `map literal` |
| `*string`, `*int` parameter to "save copy" | Adds indirection, no real saving | Pass value |
| Long parameter lists | Hard to call, hard to extend | Options struct or `WithXxx` opts |

## Verification Checklist

- [ ] `gofmt -l .` produces no output and `goimports -l .` is clean.
- [ ] No function body indented past three tab stops without justification.
- [ ] No `if ... return; else ...` in modified code.
- [ ] All maps and slices declared without literal use `make` with a sensible capacity.
- [ ] Composite literals use named fields for non-trivial struct types.
- [ ] `ctx context.Context` is the first parameter on every function that takes one.
- [ ] `golangci-lint run` passes with `gocritic`, `revive`, and `gocyclo` enabled.

## Enforce With Linters

These rules are largely mechanical and a linter will catch them in CI:

- `gofmt`, `gofumpt`, `goimports` — formatting.
- `gocritic`, `revive` — style heuristics, including `if-return`, `early-return`.
- `gocyclo`, `funlen` — function complexity / length.
- `wsl_v5` — whitespace lines for separation between blocks.

Add to `.golangci.yml` and run `golangci-lint run` in CI.

## References

- [references/formatting-and-layout.md](references/formatting-and-layout.md) — gofmt, line breaks, multi-line signatures, file order
- [references/control-flow.md](references/control-flow.md) — early returns, switch patterns, labelled loops
- [references/function-and-data-init.md](references/function-and-data-init.md) — declarations, composite literals, value-vs-pointer

Referenced files: 4

go-concurrency7.69 KB

View saved version →

---
name: go-concurrency
description: "Use when writing or reviewing concurrent Go code — goroutines, channels, select, mutexes, atomics, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Apply proactively whenever a goroutine is spawned, a shared field is mutated, or a channel is created, even if the user has not asked about concurrency. Does not cover context.Context patterns (see go-context)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Targets Go 1.21+ for typed atomics and slog. Notes Go 1.25 wg.Go, Go 1.26 testing/synctest, and Go 1.26 experimental goroutineleak profile where relevant."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Concurrency

Goroutines are cheap, but every one you spawn is a resource you must own. The goal is **structured concurrency**: each goroutine has a clear owner, a predictable exit, and a way for the caller to wait and collect errors.

## Core Rules

1. **Never start a goroutine without knowing how it will stop.** A blocked goroutine is not garbage-collected — it leaks.
2. **The caller must be able to wait.** Use `sync.WaitGroup`, `errgroup.Group`, or an explicit done channel.
3. **No goroutines in `init()`.** Expose `Start`/`Stop`/`Shutdown` so callers control the lifecycle.
4. **Share by communicating.** Default to channels; reach for `sync.Mutex` only when the problem is genuinely "protect a shared field".
5. **Only the sender closes a channel.** Closing from the receiver side panics on the next send.
6. **Specify channel direction** (`chan<-`, `<-chan`) at function boundaries — the compiler catches misuse.
7. **Always include `ctx.Done()` in `select`.** Without it, the goroutine cannot be cancelled.
8. **Test for leaks** with [`go.uber.org/goleak`](https://pkg.go.dev/go.uber.org/goleak).

## Primitive Decision

| Need | Use | Why |
|---|---|---|
| Pass a value from producer to consumer | Channel | Transfers ownership explicitly |
| Wait for N fire-and-forget goroutines | `sync.WaitGroup` (Go 1.25: `wg.Go`) | No error needed |
| Wait + collect first error + cancel siblings | `errgroup.WithContext` | Structured failure |
| Bound concurrency (worker pool) | `errgroup.SetLimit(n)` | Replaces hand-rolled pools |
| Protect a shared field | `sync.Mutex` / `sync.RWMutex` | Short critical section |
| Counter / flag | typed `sync/atomic` (`atomic.Int64`, `atomic.Bool`) | Lock-free, type-safe |
| Read-heavy concurrent map | `sync.Map` | Concurrent map read/write otherwise crashes |
| One-shot init | `sync.Once` (or `OnceFunc`/`OnceValue` in 1.21+) | Idempotent setup |
| Deduplicate concurrent calls | `x/sync/singleflight` | Cache stampede prevention |

> Read [references/sync-primitives.md](references/sync-primitives.md) when picking between mutex, atomic, `sync.Map`, `sync.Pool`, or `singleflight`, or when designing the field layout of a struct that protects shared state.

## Goroutine Lifetimes

```go
// Good: bounded WaitGroup, deterministic exit
var wg sync.WaitGroup
for item := range queue {
    wg.Add(1)
    go func(it Item) { defer wg.Done(); process(ctx, it) }(item)
}
wg.Wait()

// Bad: no stop signal, no wait — classic leak
go func() { for { flush(); time.Sleep(delay) } }()
```

Go 1.25+ exposes `wg.Go(fn)` which folds `Add`/`Done` into one call. Always call `wg.Add` **before** `go` — otherwise `wg.Wait` may return before the goroutine even starts.

## errgroup: Errors and Cancellation

`errgroup.WithContext` is the right default when sibling goroutines should cancel each other on the first failure:

```go
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, url := range urls {
    g.Go(func() error { return fetch(ctx, url) })
}
if err := g.Wait(); err != nil {
    return fmt.Errorf("fetching urls: %w", err)
}
```

`g.Wait` returns the first non-nil error; `ctx` is cancelled as soon as any worker fails. See [references/errgroup-and-pools.md](references/errgroup-and-pools.md).

## Channels

```go
func produce(out chan<- int)                  { /* send-only */ }
func consume(in <-chan int)                   { /* receive-only */ }
func transform(in <-chan int, out chan<- int) { /* ownership crosses */ }
```

**Buffer size is `0` or `1`.** Anything larger must be justified (what bounds it under load, what happens when writers block).

In every long-running `select`, include `<-ctx.Done()`. Avoid `time.After` in hot loops — it allocates a timer per iteration; hoist a `time.NewTimer` and `Reset` it instead.

> Read [references/channels-and-select.md](references/channels-and-select.md) when implementing pipelines, fan-in/fan-out, broadcast via `close`, or non-blocking sends with `default`.

## Mutexes and Atomics

The zero value of `sync.Mutex`/`RWMutex` is valid — almost never use a pointer. Do not embed mutexes; keep them as an unexported `mu` field so `Lock`/`Unlock` aren't public API. Keep critical sections short; never hold a lock across I/O. Prefer typed atomics (`atomic.Bool`, `atomic.Int64`) over raw `sync/atomic` on `int32`/`int64` fields.

## Testing: goleak and synctest

Wire `go.uber.org/goleak` into every package that spawns goroutines (`goleak.VerifyTestMain(m)` or `defer goleak.VerifyNone(t)`). For timer-dependent tests, use `testing/synctest` (Go 1.25+) so synthetic time advances deterministically. Go 1.26 adds an experimental `goroutineleak` pprof profile for production diagnosis — it is not a substitute for `goleak` in tests. See [references/leaks-and-synctest.md](references/leaks-and-synctest.md).

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Fire-and-forget `go func()` with no signal | Leaks on shutdown; can outlive its inputs | Pass `ctx`, use `errgroup`, or own a done channel |
| Closing a channel from the receiver | Panics on the next send | Only the sender closes |
| `time.After` in a hot loop | Allocates a timer per iteration | `time.NewTimer` + `Reset` |
| `select` without `ctx.Done()` | Cannot be cancelled | Always include the cancel case |
| `wg.Add(1)` inside the goroutine | `Wait` may return before `Add` runs | `Add` before `go`, or use `wg.Go` (Go 1.25+) |
| Buffered channel sized "to be safe" | Hides backpressure, masks bugs | Size 0 or 1; justify anything larger |
| Concurrent read+write on `map` | Hard runtime crash, not a race warning | `sync.Map` or `sync.RWMutex` + map |
| Mutex held across I/O / RPC | Serializes the whole service | Copy what you need under the lock; release before the call |
| Sending a pointer through a channel | Re-introduces shared memory | Send a copy or an immutable value |
| Forgetting `-race` in CI | Races ship to prod | `go test -race ./...` always |

## Verification Checklist

Before finishing a concurrency change:

- [ ] Every `go` has a documented exit (ctx, done channel, or bounded loop)
- [ ] Every long-running `select` has a `<-ctx.Done()` case
- [ ] `wg.Add` is called before `go`, or `wg.Go` is used (Go 1.25+)
- [ ] Channels are sized 0 or 1, or the size has a comment justifying it
- [ ] Only the sender closes channels; receivers use `for v := range ch` or `v, ok := <-ch`
- [ ] No mutex is held across network/disk I/O
- [ ] `go test -race ./...` is clean
- [ ] Packages that spawn goroutines wire `goleak.VerifyTestMain` or per-test `VerifyNone`

## References

- [references/sync-primitives.md](references/sync-primitives.md) — mutex vs atomic vs `sync.Map`/`Pool`/`Once`/`singleflight`
- [references/channels-and-select.md](references/channels-and-select.md) — channel ownership, direction, pipelines, non-blocking sends
- [references/errgroup-and-pools.md](references/errgroup-and-pools.md) — `errgroup`, `SetLimit`, worker pools, fan-out/fan-in
- [references/leaks-and-synctest.md](references/leaks-and-synctest.md) — `goleak`, `testing/synctest`, Go 1.26 experimental leak profile

Referenced files: 5

go-context6.14 KB

View saved version →

---
name: go-context
description: "Use when designing, propagating, or debugging context.Context flow in Go — first-parameter placement, deadlines and cancellation, request-scoped values, WithoutCancel for fire-and-forget work, and key-collision-safe value patterns. Apply proactively whenever a function takes ctx, spawns work, or accepts request-scoped data, even if the user has not asked about context."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.7+ (context in std lib). context.WithoutCancel needs Go 1.21+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Context Usage

`context.Context` carries the cancellation, deadline, and request-scoped values for a single unit of work. Pass it explicitly through the entire call chain — never store it, never replace it with `Background()` mid-flight, never use it as a side-channel for ordinary parameters.

## Core Rules

1. **`ctx` is the first parameter**, named `ctx context.Context`. No exceptions outside interface stubs imposed by external APIs.
2. **Propagate the caller's `ctx`** all the way down. Do not start a new tree with `context.Background()` inside a request path.
3. **Do not store `Context` in a struct**. Pass it to each method that needs it.
4. **Always `defer cancel()`** after `WithCancel`/`WithTimeout`/`WithDeadline`, unless ownership is explicitly transferred.
5. **Context values are for request-scoped metadata only** (request ID, auth principal, trace). Never for optional function parameters or config.
6. **Value keys must be unexported named types** to prevent cross-package collisions.

## Where Does Data Belong?

Pick the most explicit option that fits — context values are the last resort.

| Option | Use for | Why |
|---|---|---|
| Function parameter | Anything the function *needs* to do its job | Type-checked, visible at call site |
| Method receiver | State that belongs to the type | Already in scope |
| Package-level config | Process-wide, immutable | One owner, no hidden flow |
| `context.Value` | Request-scoped metadata that crosses layers without being a function arg | Untyped — use sparingly |

> Read [references/values-and-keys.md](references/values-and-keys.md) for the unexported-key pattern, typed accessors, and OpenTelemetry/trace propagation.

## Constructors

| Situation | Use |
|---|---|
| `main`, `init`, top-level test | `context.Background()` |
| Placeholder while plumbing is incomplete | `context.TODO()` |
| Inside an HTTP handler | `r.Context()` |
| Need manual cancellation | `context.WithCancel(parent)` |
| Need a deadline / timeout | `context.WithTimeout(parent, d)` / `WithDeadline` |
| Background work that must outlive the request (Go 1.21+) | `context.WithoutCancel(parent)` |

## Propagation: The One Rule

```go
// Bad — breaks the chain, downstream cannot be cancelled
func (s *OrderService) Create(ctx context.Context, o Order) error {
    return s.db.ExecContext(context.Background(), insertSQL, o.ID)
}

// Good — same ctx flows HTTP handler -> service -> DB -> external API
func (s *OrderService) Create(ctx context.Context, o Order) error {
    return s.db.ExecContext(ctx, insertSQL, o.ID)
}
```

## Deriving and Cancelling

```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // release resources even on the happy path

select {
case <-ctx.Done():
    return ctx.Err()
case res := <-doAsync(ctx):
    return res
}
```

> Read [references/cancellation-and-deadlines.md](references/cancellation-and-deadlines.md) for `WithoutCancel`, `AfterFunc`, and long-running goroutine cancellation patterns.

## Don't Wrap `Context` in Custom Types

```go
// Bad — pollutes the standard signature
type MyCtx interface {
    context.Context
    UserID() string
}

// Good — keep the signature standard, extract via helper
func UserIDFrom(ctx context.Context) (string, bool) { /* ... */ }
```

## Enforce With Linters

Most context mistakes are mechanical and a linter will catch them in CI before review:

- `govet -vet=context` — flags non-first `context.Context` parameters and lost cancels.
- `staticcheck SA1012` — calls passing `nil` context.
- `contextcheck` (`golangci-lint`) — verifies downstream calls propagate `ctx`.
- `noctx` — flags HTTP/SQL APIs called without their `*Context` variant.

Run `golangci-lint run --enable=contextcheck,noctx,staticcheck` in CI for any project that exposes `context.Context`.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `ctx context.Context` stored on a struct field | Lifetime becomes invisible; outlives the request | Pass `ctx` to each method |
| `context.Background()` mid-call | Cancellation chain breaks; goroutines leak | Use the caller's `ctx` |
| `ctx.Value("user-id")` with a string key | Cross-package collisions, no type safety | Unexported key type + typed getter |
| Passing `nil` as a context | Panics on `Done()` / `Value()` | Use `context.TODO()` while plumbing |
| `WithTimeout` without `defer cancel()` | Leaks the timer until parent finishes | `defer cancel()` on the next line |
| Custom `MyContext` interface | Breaks every standard signature | Keep `context.Context`, extract with helpers |

## Verification Checklist

- [ ] Every function that does I/O, blocks, or calls another `ctx`-aware API takes `ctx context.Context` as its **first** parameter.
- [ ] No `context.Context` field on any struct (search: `ctx\s+context\.Context` inside `type ... struct`).
- [ ] Every `WithCancel`/`WithTimeout`/`WithDeadline` is followed by `defer cancel()` on the next line.
- [ ] No `context.Background()` or `context.TODO()` calls inside request handlers.
- [ ] All context value keys are unexported named types, accessed via typed getters.
- [ ] `golangci-lint run --enable=contextcheck,noctx` passes.

## References

- [references/values-and-keys.md](references/values-and-keys.md) — unexported key types, typed accessors, trace propagation
- [references/cancellation-and-deadlines.md](references/cancellation-and-deadlines.md) — timeouts, `WithoutCancel`, `AfterFunc`, goroutine cancellation
- [references/http-and-db.md](references/http-and-db.md) — handlers, `NewRequestWithContext`, `QueryContext`/`ExecContext`

Referenced files: 4

go-control-flow5.31 KB

View saved version →

---
name: go-control-flow
description: "Use when writing conditionals, loops, switches, type switches, or blank-identifier patterns in Go. Covers if-with-initialization, guard clauses, early returns, the unified for loop, range over slices/maps/strings/channels, parallel assignment, labeled break, and `_` for discards and side-effect imports. Apply proactively to any new if/for/switch, even when the user does not mention scoping or shadowing. Does not cover error-flow specifics (see go-error-handling)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Plain Go (any supported version)."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Control Flow

Go gives you `if`, `for`, and `switch` — and one looping construct that covers them all. The idioms are small but strict: scope variables tightly, return early, keep the happy path unindented.

## Core Rules

1. **Scope variables with if-init when they live only for the check.** `if x, err := f(); err != nil { ... }`.
2. **Guard clauses over nested `else`.** When the `if` body returns/breaks/continues, drop the `else`.
3. **`:=` redeclares only in the same scope.** In an inner scope it shadows — a frequent bug source.
4. **One `for`, three forms.** Condition-only (while), three-clause (C-style), and infinite (`for {}`).
5. **`range` over string yields runes**, over map yields **non-deterministic order**, over channel **drains until closed**.
6. **`break` inside `switch` only breaks the switch.** Use a labeled `break` to exit the enclosing `for`.
7. **The blank identifier `_` discards, but never errors.** Silent error dropping is a bug.

## Decision: var vs := vs = at a glance

| Situation | Use |
|---|---|
| New variable, scoped to the check | `if v, err := f(); err != nil` |
| Reusing an outer variable | plain `=` (avoid `:=` to prevent shadowing) |
| At least one new + reuse of outer | `:=` is fine (same scope only) |
| Wanted zero value | `var x T` |

## If with Initialization

```go
if err := file.Chmod(0664); err != nil {
    return err
}
```

If `err` is needed past the `if`, declare it separately:

```go
x, err := f()
if err != nil {
    return err
}
// use x freely
```

## Guard Clauses

```go
f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)
```

Never bury the success path inside `else`.

## The Shadowing Trap

```go
// Bug: inner ctx never escapes the if block
if *shortenDeadlines {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
}

// Fix: assign with =, declaring cancel separately
var cancel func()
ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
defer cancel()
```

> Read [references/blank-identifier.md](references/blank-identifier.md) for `_` use cases (interface checks, side-effect imports, multi-return discards).

## For Loops

```go
// Condition-only (Go's "while")
for x > 0 { x = process(x) }

// Infinite
for {
    if done() { break }
}

// Three-clause
for i := 0; i < n; i++ { ... }
```

### Range

```go
for i, v := range slice { ... }   // index + value
for k, v := range myMap  { ... }  // non-deterministic order
for i, r := range "héllo" { ... } // i is byte offset; r is rune
for v := range ch { ... }         // drains until closed
```

### Parallel Assignment

Go has no comma operator. Use parallel assignment instead:

```go
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}
```

`++` and `--` are statements, not expressions — they cannot appear inside a parallel assignment.

## Switch and Labeled Break

```go
Loop:
    for _, v := range items {
        switch v.Type {
        case "done":
            break Loop // breaks the for, not just the switch
        }
    }
```

> Read [references/switch-patterns.md](references/switch-patterns.md) for expression-less switches, comma cases, fallthrough, and type switches.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `} else { return ... }` after a returning `if` | Pointless nesting | Drop `else`; let the happy path stay flat |
| `if _, err := f(); err == nil { ... }` then use the value | Value is out of scope | Move the `:=` outside the `if` |
| `:=` in inner scope reassigning outer var | Silently shadows | Use `=` (declare the new locals separately) |
| Iterating a map and relying on order | Order is randomized | Sort keys explicitly |
| `break` inside `switch` expecting to exit `for` | Only exits switch | Use labeled `break Label` |
| `_ = doSomething()` to silence an error | Real failures vanish | Handle it or document why |

## Verification Checklist

- [ ] No `else` branch after an `if` that returns/breaks/continues
- [ ] `:=` in inner scopes does not shadow important outer variables
- [ ] Loops use the simplest of the three forms that fits
- [ ] `range` over strings treats the index as a byte offset, not a rune index
- [ ] Map iteration does not assume an order
- [ ] Errors are never silently discarded with `_`
- [ ] Labeled `break` is used when a switch needs to exit a surrounding loop

## References

- [references/switch-patterns.md](references/switch-patterns.md) — expression-less, comma cases, fallthrough, type switches
- [references/blank-identifier.md](references/blank-identifier.md) — `_` for multi-return, interface compliance, side-effect imports

Referenced files: 3

go-database8.26 KB

View saved version →

---
name: go-database
description: "Use when writing, reviewing, or debugging Go code that talks to a SQL database (PostgreSQL, MySQL, MariaDB, SQLite). Covers library choice (database/sql, sqlx, sqlc, pgx, GORM trade-offs), parameterized queries, context propagation, NULL handling, scanning, transactions and isolation, connection pool tuning, and migration tooling. Apply when adding repository code, refactoring SQL, or auditing for missing rows.Close()/QueryContext."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+. Library-agnostic: applies to database/sql, sqlx, sqlc, pgx."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Database

Go's `database/sql` is a thin, driver-pluggable foundation. Most projects layer one of `sqlx`, `sqlc`, or `pgx` on top for ergonomics. ORMs (GORM, ent) trade SQL visibility for one less line of code — a bad trade in production.

## Core Rules

1. **SQL is the source of truth.** It is reviewed, version-controlled, and explained in code. Magic ORM queries are the opposite.
2. **Always parameterize.** `$1`/`?` placeholders, never string concatenation. The driver handles escaping; you cannot.
3. **Every I/O call takes `ctx`.** `QueryContext`, `ExecContext`, `GetContext`. No context = no timeout = a stuck handler.
4. **Distinguish "not found" from "error".** `errors.Is(err, sql.ErrNoRows)` is a domain signal, not a failure.
5. **Close rows.** `defer rows.Close()` immediately after `QueryContext`. Forgetting it leaks a pool connection.
6. **Configure the pool.** Default `MaxOpenConns` is unlimited — a runaway request rate exhausts the DB.

## Library Decision

| Library | Best for | Struct scanning | Code-gen |
|---|---|---|---|
| `database/sql` | Minimal deps, multi-driver portability | Manual `Scan` | No |
| `sqlx` | Sweetens `database/sql` ergonomics | `StructScan`, `Get`, `Select` | No |
| `sqlc` | Type-safe queries derived from `.sql` files | Generated structs and funcs | Yes |
| `pgx` (v5) | PostgreSQL-only, 30-50% faster, native types | `pgx.RowToStructByName` | No |
| GORM / ent | **Avoid** in new code | Reflection | Yes |

**Why not ORMs.**

- Generated queries are unpredictable; N+1 problems are invisible at the call site.
- Hooks (`BeforeCreate`, `AfterUpdate`) create implicit state machines.
- Schema migrations entangle with application code.
- Learning the ORM API is harder than learning SQL, and the abstraction leaks at every interesting query.

> Read [references/library-tradeoffs.md](references/library-tradeoffs.md) when picking between sqlx, sqlc, and pgx for a new project.

## Parameterized Queries

```go
// VERY BAD — SQL injection.
q := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)

// Good — placeholder, driver-escaped.
err := db.GetContext(ctx, &u, "SELECT id, email FROM users WHERE email = $1", email)
```

### Dynamic `IN` clauses

```go
q, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
if err != nil { return fmt.Errorf("expanding IN: %w", err) }
q = db.Rebind(q)                            // $1, $2, ... for Postgres
err = db.SelectContext(ctx, &users, q, args...)
```

### Dynamic column names

Placeholders cannot stand in for identifiers. Use an allowlist:

```go
allowed := map[string]bool{"name": true, "email": true, "created_at": true}
if !allowed[sortCol] {
    return fmt.Errorf("invalid sort column: %s", sortCol)
}
q := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s", sortCol)
```

## Context Propagation

```go
// Bad — query runs to completion even if the client disconnected.
rows, err := db.Query("SELECT ...")

// Good — driver cancels the query on ctx.Done().
rows, err := db.QueryContext(ctx, "SELECT ...")
```

Every I/O method takes `ctx` first. Pass the request context through service → repository.

## Error Handling

```go
err := r.db.GetContext(ctx, &u, "SELECT ... WHERE id = $1", id)
switch {
case errors.Is(err, sql.ErrNoRows):
    return nil, ErrUserNotFound           // domain error
case err != nil:
    return nil, fmt.Errorf("get user %s: %w", id, err)
}
```

### Always close rows

```go
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil { return fmt.Errorf("query: %w", err) }
defer rows.Close()
for rows.Next() {
    var u User
    if err := rows.Scan(&u.ID, &u.Name); err != nil { return fmt.Errorf("scan: %w", err) }
    users = append(users, u)
}
if err := rows.Err(); err != nil { return fmt.Errorf("iterate: %w", err) }
```

Three error checks (`Query`, `Scan`, `rows.Err()`) — missing the third hides truncated iteration.

## NULL Columns and Scanning

```go
type User struct {
    ID    string         `db:"id"`
    Email string         `db:"email"`
    Bio   *string        `db:"bio"`      // nullable → pointer
    Login sql.NullTime   `db:"last_login"`
}
```

Pointer fields work cleanly with JSON marshaling and with sqlx `StructScan`. Use `sql.NullXxx` when you need to distinguish "not set" from "zero value" at the SQL layer.

> Read [references/scanning.md](references/scanning.md) for sqlx tags, pgx `RowToStructByName`, and `sql.Null*` patterns.

## Transactions and Isolation

Wrap related writes in `db.BeginTxx(ctx, &sql.TxOptions{Isolation: ...})`, rollback on every error path, commit only on success. Use `SELECT ... FOR UPDATE` when reading data you intend to modify — otherwise a concurrent writer races you. See [references/transactions.md](references/transactions.md) for isolation levels, retryable serialization errors, and the UnitOfWork pattern.

## Connection Pool

```go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
```

`MaxOpenConns` should be ≤ the DB server's `max_connections` divided by replica count, with headroom for migrations and other consumers.

## Migrations

Do **not** generate migration SQL with this skill. Schema design needs human judgment about indexes, foreign keys, and data volume.

Recommended tools:

- [golang-migrate](https://github.com/golang-migrate/migrate) — Go library + CLI.
- [Atlas](https://atlasgo.io/) — declarative, supports diff and lint.
- [Flyway](https://flywaydb.org/) — JVM, common in heterogeneous shops.

Run migrations in CI/CD, not from application code at startup.

## Avoid Hidden SQL Features

Triggers, views, materialized views, stored procedures, row-level security — all create invisible state changes. The application code looks correct; debugging takes hours. Keep behavior in Go where it is testable and reviewable.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `fmt.Sprintf` building queries | SQL injection | Always `$1`/`?` placeholders |
| `db.Query` (no context) | No timeout, no cancellation | `db.QueryContext(ctx, ...)` |
| Forgetting `defer rows.Close()` | Connection leak; pool exhaustion | Defer immediately after `QueryContext` |
| Missing `rows.Err()` check | Truncated iteration treated as success | Always check after the `for rows.Next()` loop |
| `db.Query` for INSERT/UPDATE/DELETE | `*Rows` must be closed; easy to leak | Use `db.ExecContext` |
| Returning raw `*sql.DB` from repos | Couples service to driver | Return domain types; keep `*sql.DB` private |
| ORM with hooks for business logic | Magic side effects, untraceable bugs | Move logic into a service layer |
| `MaxOpenConns(0)` (unlimited) | Stampede exhausts the DB | Cap below `pg_max_connections` |

## Verification Checklist

- [ ] No string-concatenated SQL
- [ ] Every DB call uses a `*Context` method
- [ ] Every `QueryContext` is followed by `defer rows.Close()`
- [ ] Every iteration loop ends with `rows.Err()` check
- [ ] `sql.ErrNoRows` is translated to a domain error at the repository boundary
- [ ] Connection pool limits set; not relying on defaults
- [ ] Transactions roll back on every error path; commit only on success
- [ ] Migrations live in `migrations/`, not in Go code

## References

- [references/library-tradeoffs.md](references/library-tradeoffs.md) — sqlx vs sqlc vs pgx vs GORM with concrete examples
- [references/transactions.md](references/transactions.md) — isolation levels, FOR UPDATE, retryable errors
- [references/scanning.md](references/scanning.md) — sqlx, pgx, NULL handling, JSON columns
- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each anti-pattern

Referenced files: 5

go-data-structures8.16 KB

View saved version →

---
name: go-data-structures
description: "Use when choosing or operating on Go slices, maps, arrays, strings, or container/* types — including slice internals, capacity growth, preallocation, map buckets, sets via map[T]struct{}, strings.Builder vs bytes.Buffer, generic containers, and the slices/maps standard packages (Go 1.21+). Apply proactively whenever data is being collected, transformed, or copied, even if the user has not asked about allocation."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. slices/maps packages need Go 1.21+; iterator helpers need 1.23+; weak.Pointer needs 1.24+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Data Structures

Pick the structure that fits the access pattern — not the most familiar one. Slices and maps are the workhorses; arrays, container types, and the `slices`/`maps` packages cover the rest. Understanding the **header layout**, **growth costs**, and **copy semantics** of each turns most performance questions into one-line decisions.

## Core Rules

1. **Slices and maps are reference types** — assigning copies the header, not the data. Use `slices.Clone` / `maps.Clone` for a true copy.
2. **Preallocate** with `make([]T, 0, n)` and `make(map[K]V, n)` whenever the size is known or estimable.
3. **Always assign the result of `append`** — the backing array may move.
4. **Use `slices` and `maps` packages** (Go 1.21+) instead of hand-rolled helpers.
5. **`map[K]struct{}` is the canonical set** — zero-byte values, no boolean ambiguity.
6. **`strings.Builder` for string building**, `bytes.Buffer` when you need `io.Reader`/`io.Writer`.

## Picking a Structure

```
What do you need?
├─ Ordered, fixed compile-time size      → [N]T  array
├─ Ordered, dynamic size                 → []T   slice
│  ├─ Known size               → make([]T, 0, n)
│  └─ JSON output must be []   → []T{} literal (not nil)
├─ Key/value lookup                      → map[K]V
│  ├─ Need a set            → map[K]struct{}
│  └─ Known size            → make(map[K]V, n)
├─ Priority queue / top-k                → container/heap
├─ Frequent middle insertion             → container/list
├─ Fixed-size rolling window             → container/ring
├─ Pure string building                  → strings.Builder
└─ Read+write of bytes                   → bytes.Buffer
```

## Slice Internals

A slice is a 3-word header: pointer, length, capacity. Multiple slices can alias the same backing array — `s[1:4]` shares memory with `s`.

### Capacity Growth

The exact algorithm has changed across versions; do **not** rely on it. As of recent Go:

- `len < 256` → capacity roughly doubles.
- `len ≥ 256` → grows by ~25%.
- Each growth allocates a new backing array and copies — O(n) per growth.

### Preallocation

```go
users := make([]User, 0, len(ids))         // exact size
results := make([]Result, 0, estimated)    // approximate
s = slices.Grow(s, additional)             // pre-grow before bulk append (Go 1.21+)
```

### `slices` Package (Go 1.21+)

| Function | Purpose |
|---|---|
| `Sort`, `SortFunc`, `SortStableFunc` | sorting |
| `BinarySearch`, `BinarySearchFunc` | sorted lookup |
| `Contains`, `Index`, `IndexFunc` | search |
| `Compact`, `CompactFunc` | dedupe adjacent equals |
| `Clone`, `Equal` | safe copy / comparison |
| `Delete`, `DeleteFunc` | removal preserving order |
| `Grow` | preallocate before append |
| `Concat` (1.22+) | concatenate slices |

Prefer these over hand-rolled loops — they're tested, generic, and use the fastest available paths.

> Read [references/slices-and-maps.md](references/slices-and-maps.md) for capacity growth, aliasing pitfalls, and 2-D slice patterns.

## nil vs Empty Slice: The JSON Trap

Both have `len == 0` and `cap == 0`, but they encode differently:

```go
var nilSlice []string         // → JSON: null
emptySlice := []string{}      // → JSON: []
```

API contracts almost always want `[]`. **Initialise the slice explicitly** in any struct that gets marshaled to JSON, and treat nil/empty as identical when *reading* (use `len(s) == 0`).

For internal computation where nil is never marshaled, the nil slice is conventional and slightly cheaper (no allocation until first append).

## Maps

Maps are hash tables with 8-entry buckets and overflow chains. They are reference types — assigning a map copies a pointer.

### Preallocation

```go
m := make(map[string]*User, len(users)) // avoids rehashing during population
```

The size hint is *approximate* (it's about bucket count), but it still saves repeated rehashing in the common case.

### Sets

```go
type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(v T)         { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool    { _, ok := s[v]; return ok }
func (s Set[T]) Remove(v T)      { delete(s, v) }
```

`struct{}` is zero bytes; the set is just the key set of the underlying map.

`map[K]bool` is also common but ambiguous: did `false` mean "explicitly excluded" or "not present"? `struct{}` removes the question.

### `maps` Package (Go 1.21+)

`Clone`, `Equal`/`EqualFunc`, `DeleteFunc`; `Keys`, `Values`, `Collect`, `Insert` since 1.23 (iterators).

> Read [references/strings-bytes-builder.md](references/strings-bytes-builder.md) for string-vs-bytes, `Builder` vs `Buffer`, and rune handling.

## Arrays

Fixed-size, value type, copied on assignment. Useful for compile-time-known sizes:

```go
type Digest [32]byte
type IP4 [4]byte
cache := map[[2]int]Result{} // arrays are comparable → usable as map keys
```

For anything dynamic, use a slice.

## container/* and Third-Party

| Package | Use case | Caveat |
|---|---|---|
| `container/heap` | priority queue, top-K | implement the interface yourself |
| `container/list` | LRU, frequent middle splice | poor cache locality |
| `container/ring` | rolling window, round-robin | fixed size |
| `bufio` | I/O with many small reads/writes | always check `Flush` errors |

For typed sets/queues/trees beyond the stdlib, prefer well-tested libraries (`emirpasic/gods`, `gammazero/deque`) and benchmark before optimising.

> Read [references/containers-and-pointers.md](references/containers-and-pointers.md) for heap implementation, `unsafe.Pointer`'s six valid patterns, and `weak.Pointer[T]`.

## Copy Semantics Cheat Sheet

| Type | Copy behaviour |
|---|---|
| primitives, arrays, structs | value (deep for contained value fields) |
| slice | header copied, backing array shared — use `slices.Clone` |
| map, channel | reference copied — use `maps.Clone` for maps |
| `*T`, `interface` | address / (type, value) pair copied |

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `s := append(s, x)` ignoring return | Backing array may move; `s` becomes stale | Always reassign |
| `var m map[K]V; m[k] = v` | nil map panic | `m := make(map[K]V)` or `map[K]V{}` |
| `var s []T` then marshal to JSON as `[]` | Encodes as `null` | `s := []T{}` |
| `make([]T, 0, 10000)` "just in case" | Wasted memory | Size by actual data |
| `m := map[K]bool{}` as a set | `false` is ambiguous | `map[K]struct{}` |
| `bytes.Buffer` for pure string building | Extra copy in `String()` | `strings.Builder` |
| Large struct values in a map | Each lookup copies the value | `map[K]*V` |

## Verification Checklist

- [ ] Every `make([]T, ...)` and `make(map[K]V, ...)` has a capacity hint when the size is known.
- [ ] Every `append` reassigns its result.
- [ ] Slices marshaled to JSON are initialised as `[]T{}`, not `var s []T`.
- [ ] All "sets" use `map[K]struct{}` (or a generic `Set[T]` wrapper).
- [ ] No `bytes.Buffer` used purely for `String()` output.
- [ ] No `*sync.Mutex` copied via struct assignment (`go vet copylocks`).
- [ ] `slices.Clone` / `maps.Clone` used when handing data to callers that may mutate.

## References

- [references/slices-and-maps.md](references/slices-and-maps.md) — internals, growth, aliasing, `slices`/`maps` packages
- [references/strings-bytes-builder.md](references/strings-bytes-builder.md) — `strings.Builder`, `bytes.Buffer`, rune handling
- [references/containers-and-pointers.md](references/containers-and-pointers.md) — `container/heap`, generic wrappers, `unsafe.Pointer`, `weak.Pointer`

Referenced files: 4

go-declarations6.11 KB

View saved version →

---
name: go-declarations
description: "Use when declaring or initializing Go variables, constants, structs, or maps. Covers var vs :=, grouped declaration blocks, iota enums starting at 1, struct/map/slice composite literals, raw string literals, `any` over `interface{}`, and avoiding shadowed builtins. Apply proactively to any new struct, const block, or top-level var, even if the user did not ask about declaration style. Does not cover identifier naming (see go-naming)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. `any` requires Go 1.18+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Declarations and Initialization

Pick the simplest declaration form that expresses your intent: scope variables tightly, group related declarations, and let the zero value do its job.

## Core Rules

1. **`:=` for locals with values; `var` for intentional zero values or top-level declarations.**
2. **Group related declarations in parenthesized blocks.** Separate unrelated ones into distinct blocks.
3. **Start enums at `iota + 1`** so the zero value is "invalid/unset" — unless zero is genuinely meaningful.
4. **Initialize structs with field names.** Omit zero-value fields; let defaults speak.
5. **Use `any`, not `interface{}`,** in all new code.
6. **Never shadow builtins** (`len`, `cap`, `error`, `new`, `make`, `copy`, `any`, `nil`, ...).

## Decision: var vs :=

| Context | Use | Example |
|---|---|---|
| Package-level | `var` (always) | `var startTime = time.Now()` |
| Local with computed value | `:=` | `s := "foo"` |
| Local zero-value, intentional | `var` | `var filtered []int` |
| Declared type differs from RHS | `var T = expr` | `var e error = f()` |

> Read [references/scope-and-shadowing.md](references/scope-and-shadowing.md) when fighting subtle bugs caused by `:=` redeclaring an outer variable.

## Group Related Declarations

```go
// Bad
const a = 1
const b = 2

// Good
const (
    a = 1
    b = 2
)
```

Inside functions, group adjacent vars even if loosely related:

```go
var (
    caller  = c.name
    format  = "json"
    timeout = 5 * time.Second
)
```

## Constants and iota

Zero is the default; reserve it for "uninitialized" by starting enums at `iota + 1`:

```go
type Operation int

const (
    Add      Operation = iota + 1 // 1
    Subtract                      // 2
    Multiply                      // 3
)
```

Use plain `iota` only when the zero value is the sensible default (e.g., `LogToStdout = iota`).

> Read [references/iota-and-literals.md](references/iota-and-literals.md) for bitmask enums, `String()` methods, raw strings, and composite-literal formatting.

## Initializing Structs

- **Always use field names.** Positional struct literals break on field reordering and are caught by `go vet`.
- **Omit zero-value fields** — clarity beats explicit zeros.
- **`var u User`** for a zero-value struct (not `u := User{}`).
- **`&T{...}` over `new(T)`** when you want a pointer.

```go
u := User{Name: "Ada", Email: "ada@example.com"}
sptr := &Config{Timeout: 5 * time.Second}
var empty Buffer // zero value, ready to use
```

Test tables with ≤3 fields may use positional literals when the meaning is obvious.

## Initializing Maps

| Scenario | Use | Example |
|---|---|---|
| Empty, will be populated | `make(map[K]V)` | `m := make(map[string]int)` |
| Nil, lazily allocated | `var` | `var m map[string]int` |
| Known entries up front | Literal | `m := map[string]int{"a": 1}` |

`make` signals "initialized but empty" — different from a nil map (which panics on write). Provide a size hint when the count is known: `make(map[K]V, n)`.

## Raw String Literals

Use backticks to avoid escape gymnastics:

```go
// Bad
re := "^\\s*name:\\s*\"(.*)\""

// Good
re := `^\s*name:\s*"(.*)"`
```

Ideal for regex, SQL, JSON, and multi-line text.

## `any`, not `interface{}`

```go
// Old
func Print(v interface{}) { ... }

// New
func Print(v any) { ... }
```

`any` is an alias for `interface{}` since Go 1.18 — same type, less noise.

## Don't Shadow Builtins

The predeclared identifiers (`error`, `string`, `len`, `cap`, `append`, `copy`, `new`, `make`, `close`, `delete`, `panic`, `recover`, `any`, `true`, `false`, `nil`, `iota`) are not reserved words — Go lets you shadow them. Don't.

```go
// Bad — shadows the builtin error type
var error string

// Good
var errorMessage string
```

`go vet` catches the most common cases.

> Read [references/structs-and-tags.md](references/structs-and-tags.md) when designing struct fields that cross a serialization boundary (JSON, YAML, protobuf), embedding types, or formatting many-field literals.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `u := User{}` for a zero value | Misleads readers into expecting non-defaults | `var u User` |
| `new(T)` then assign fields | Two-step where one works | `&T{Field: v}` |
| Positional struct literals (>3 fields) | Silent breakage on field reordering | Use field names |
| `iota` starting at 0 for an enum | Zero value collides with a real case | `iota + 1` |
| `var m map[string]int` then `m[k] = v` | Panic on nil map write | `m := make(map[string]int)` |
| Hand-escaped JSON or regex strings | Hard to read, easy to mistype | Raw string literal |
| `interface{}` in new code | Verbose, outdated | `any` |

## Verification Checklist

- [ ] Top-level declarations use `var`/`const`; locals use `:=` unless zero-value is intended
- [ ] Related `const`/`var`/`type` are in grouped blocks
- [ ] Enums start at `iota + 1` (or the zero value is explicitly meaningful)
- [ ] Struct literals use field names; zero-value fields are omitted
- [ ] Maps that will be written to are constructed with `make`
- [ ] No builtins shadowed (`error`, `len`, `cap`, ...)
- [ ] `any` used instead of `interface{}`

## References

- [references/scope-and-shadowing.md](references/scope-and-shadowing.md) — variable scope, `:=` redeclaration rules, shadowing traps
- [references/iota-and-literals.md](references/iota-and-literals.md) — iota patterns, bitmasks, raw strings, composite literals
- [references/structs-and-tags.md](references/structs-and-tags.md) — struct initialization, field tags, embedding

Referenced files: 4

go-defensive6.5 KB

View saved version →

---
name: go-defensive
description: "Use when hardening Go code at API boundaries: copy slices/maps on entry and return, defer cleanup, verify interface compliance at compile time, model time with time.Time/time.Duration, design enum zero values, prefer crypto/rand, and inject clocks for testability. Apply proactively when reviewing for robustness. Error-handling strategy: see go-error-handling."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. `crypto/rand.Text` examples assume Go 1.24+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Defensive Programming

Hardening Go code is not paranoia — it is the discipline of making your boundaries honest. Copy what crosses them, clean up what you opened, model time and randomness honestly, and never let a panic escape a package.

## Core Rules

1. **Copy slices and maps at API boundaries.** They are reference types — leaking the backing array leaks mutation.
2. **`defer` the cleanup right after the acquire.** `f, err := os.Open(...); defer f.Close()`.
3. **Verify interface compliance at compile time:** `var _ I = (*T)(nil)`.
4. **Model time and durations with `time.Time` and `time.Duration`,** never raw ints.
5. **Inject `now func() time.Time`** instead of calling `time.Now()` directly in production code.
6. **Enums start at `iota + 1`** so the zero value is invalid.
7. **`crypto/rand` for secrets, never `math/rand`.**
8. **Panics never cross package boundaries.** Convert to errors at the edge.
9. **Avoid mutable package-level state.** Inject dependencies instead.

## Boundary Hardening Checklist

When you touch an exported function or method, walk this list in order:

| # | Check |
|---|---|
| 1 | Return errors, don't panic across boundaries |
| 2 | Copy slices/maps you'll retain |
| 3 | Copy slices/maps you'll return if internal state aliases them |
| 4 | `defer` Close / Unlock / cancel right after the acquire |
| 5 | Compile-time interface satisfaction check |
| 6 | `time.Time` / `time.Duration` types, injected clock |
| 7 | Enum zero = invalid (`iota + 1`) |
| 8 | `crypto/rand` for any secret material |

## Copy at API Boundaries

```go
// Receiving: copy a slice we'll retain
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = make([]Trip, len(trips))
    copy(d.trips, trips)
}

// Returning: copy a map so callers can't mutate our state
func (s *Stats) Snapshot() map[string]int {
    out := make(map[string]int, len(s.counters))
    for k, v := range s.counters {
        out[k] = v
    }
    return out
}
```

> Read [references/boundary-copying.md](references/boundary-copying.md) when deciding which boundaries actually need copies (and when copying is wasted work).

## Defer Cleanup

`defer` evaluates arguments at the `defer` statement and runs the call when the surrounding function returns (LIFO order):

```go
f, err := os.Open(name)
if err != nil {
    return err
}
defer f.Close()
```

Place `defer` immediately after the acquire — the proximity makes pair-correctness reviewable at a glance.

For locks:

```go
mu.Lock()
defer mu.Unlock()
```

Beware of `defer` inside loops — accumulated defers run only when the function returns, not when the iteration ends.

## Verify Interface Compliance

```go
var _ http.Handler = (*Handler)(nil)
```

If `(*Handler)` ever stops satisfying `http.Handler`, the build fails. The line costs nothing at runtime and gives you a free contract.

## Time Modeling

```go
// Bad — what unit is timeout?
type Config struct {
    Timeout int
}

// Good
type Config struct {
    Timeout time.Duration
}
```

For wall-clock work, inject the clock so tests can pin time:

```go
type Signer struct {
    now func() time.Time
}

func NewSigner() *Signer {
    return &Signer{now: time.Now}
}

// In tests:
s := &Signer{now: func() time.Time { return fixedTime }}
```

> Read [references/time-and-enums.md](references/time-and-enums.md) for monotonic time, time zones, struct tags, and embedding tradeoffs.

## Crypto Random

```go
import "crypto/rand"

// Go 1.24+
func APIKey() string { return rand.Text() }
```

`math/rand` and `math/rand/v2` are predictable from a seed — never use them for keys, tokens, nonces, or any secret material.

## Must Functions

`Must*` helpers panic on error. They are appropriate **only** at program initialization, where failure means the program cannot start:

```go
var (
    validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
    tmpl    = template.Must(template.ParseFiles("index.html"))
)
```

Don't write `MustFoo` for runtime call sites — it shifts an error condition into a crash.

> Read [references/must-and-panic.md](references/must-and-panic.md) for writing custom `Must*`, recovering at goroutine boundaries, and distinguishing `panic` from `log.Fatal`.

## Avoid Mutable Globals

```go
// Bad — testing requires save/restore dance
var DB *sql.DB

// Good — pass the dependency
type Service struct {
    db *sql.DB
}
```

Constants and once-initialized lookup tables are fine. Mutable package-level vars are a code smell.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Storing the caller's slice without copying | Mutation aliasing | `make` + `copy` |
| Returning the internal map directly | External mutation of state | Return a snapshot |
| `time.Now()` in business logic | Hostile to tests | Inject `now func() time.Time` |
| `var Timeout = 5` read as seconds elsewhere | Ambiguous unit | `time.Duration` |
| `math/rand` for keys | Predictable from seed | `crypto/rand` |
| `panic` to signal a domain error | Crashes the caller | Return an error |
| `defer` inside a tight loop | Defers stack until function return | Wrap loop body in a function |

## Verification Checklist

- [ ] Slices/maps stored from callers, or returned aliasing internal state, are copied
- [ ] Every `Open`/`Lock` has a `defer Close`/`Unlock` next to it
- [ ] Compile-time interface checks cover exported implementations
- [ ] Durations are `time.Duration`, timestamps are `time.Time`; clock is injected
- [ ] Enum zero values are invalid (or explicitly meaningful)
- [ ] No secret material derived from `math/rand`
- [ ] No mutable package-level vars; no `panic` across library boundaries

## References

- [references/boundary-copying.md](references/boundary-copying.md) — when defensive copies pay off vs. wasted allocation
- [references/time-and-enums.md](references/time-and-enums.md) — modeling time, durations, enums, struct tags
- [references/must-and-panic.md](references/must-and-panic.md) — `Must*` helpers, recover at boundaries, panic vs `log.Fatal`

Referenced files: 4

go-documentation7.73 KB

View saved version →

---
name: go-documentation
description: "Use when writing or reviewing Go documentation — godoc comments on packages, types, functions, methods, sentinel errors; runnable Example tests; README/CONTRIBUTING/CHANGELOG. Covers the project-type detection (library vs application) that decides which docs are needed, comment grammar (start with name, full sentences), what to document vs what to skip, and Example test conventions. Apply proactively when introducing exported names, even if documentation was not requested."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Works on any Go version; new-style godoc headings/links need Go 1.19+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*)
---

# Go Documentation

Documentation in Go is part of the API. Doc comments compile into `go doc`, `pkg.go.dev`, and IDE tooltips, so the rules exist to make those views readable. Code says *what*; comments say *why*, *when*, and *what can go wrong*.

## Core Rules

1. **Every exported name has a doc comment.** Packages, types, functions, methods, constants, variables.
2. **Doc comments start with the name** (`// Encode writes ...`). Full sentences, capitalised, end with a period.
3. **Document non-obvious behaviour.** Restating the signature is noise.
4. **Mark deprecations explicitly** with a `Deprecated:` paragraph.
5. **Examples are tests** — runnable `Example*` functions in `_test.go` files with `// Output:` blocks are verified by `go test`.
6. **Every package has exactly one package comment** above the `package` clause in one file (`doc.go` if long).

## What Project Are You Documenting?

Detect this first — it changes the doc surface area.

| Signal | Project type | Docs to focus on |
|---|---|---|
| No `main` package, intended to be imported | **Library** | godoc, `Example*` tests, README usage examples, pkg.go.dev rendering |
| Has `main` package, `cmd/` directory, ships a binary or Docker image | **Application/CLI** | Install instructions, `--help` text, config docs |
| Both (libraries that ship CLI tools) | Both | All of the above |

Universal: doc comments on exported names, package comment, README, LICENSE, CONTRIBUTING (recommended), CHANGELOG (recommended).

> Read [references/godoc-grammar.md](references/godoc-grammar.md) for the comment grammar, headings/lists, deprecation markers, and full examples.

## Comment Grammar

```go
// Encode writes the JSON encoding of req to w.
// It returns an error if req contains a non-serialisable field.
func Encode(w io.Writer, req *Request) error
```

Rules:

- Start with the name of the thing.
- Use a verb phrase for functions/methods, a noun phrase for types/values.
- Articles (`A`, `An`, `The`) may precede the name.
- Full sentences, punctuation included.
- Wrap at ~80 columns for diff comfort; no hard limit.

Unexported names with non-obvious behaviour should also be commented — those comments are for maintainers, not godoc.

## What to Document

| Topic | Document when | Skip when |
|---|---|---|
| Parameters | edge cases, units, ranges | type/name already say it |
| Context | behaviour differs from standard cancellation | standard `ctx.Err()` propagation |
| Concurrency | ambiguous (e.g., a read that mutates internal state) | read-only is safe by default; mutation is unsafe by default |
| Cleanup | always — `defer Close()`, `Stop()` requirements | — |
| Errors | sentinel values (`ErrNotFound`), error types (use `*PathError` pointer) | — |
| Named returns | multiple values of the same type | type alone is clear |
| Side effects | always — file writes, network calls, init-time work | — |

Restating signatures is the most common waste:

```go
// Bad — restates what you can see
// SetName sets the name.
func (u *User) SetName(name string) { ... }

// Good — explains the why and the constraints
// SetName sets the user's display name. Returns ErrInvalidName
// if name is empty or longer than MaxNameLen.
func (u *User) SetName(name string) error { ... }
```

## Package Comments

Every package has exactly one. Place it above the `package` clause in one file. For long descriptions, use a dedicated `doc.go`.

```go
// Package store provides a transactional key-value store backed by
// SQLite. It is safe for concurrent use; see (*Store).BeginTx for
// transaction semantics.
package store
```

For `main` packages, use the binary name:

```go
// The migrate command applies SQL migrations from disk to a database.
package main
```

## Runnable Examples

```go
func ExampleEncode() {
    var buf bytes.Buffer
    _ = Encode(&buf, &Request{ID: "abc"})
    fmt.Println(buf.String())
    // Output: {"id":"abc"}
}
```

Naming:

- `func Example()` — package-level example.
- `func ExampleFoo()` — example for `Foo`.
- `func ExampleFoo_bar()` — alternate example for `Foo` titled "bar".
- `func ExampleT_Method()` — example for `T.Method`.

`go test` runs these and verifies the `// Output:` line matches. They appear in godoc attached to the named symbol — the best documentation is the kind the compiler keeps honest.

> Read [references/examples-and-readme.md](references/examples-and-readme.md) for `Example*` patterns, the canonical README outline, and CONTRIBUTING/CHANGELOG templates.

## Error and Type Docs

Sentinel errors: document on the variable, not on each return.

```go
// ErrNotFound is returned when no record matches the query.
var ErrNotFound = errors.New("store: not found")
```

Error types: document with the **pointer form** so `errors.Is`/`errors.As` examples match.

```go
// PathError records the operation and path that caused an error.
// Use errors.As(err, new(*PathError)) to inspect the fields.
type PathError struct { Op, Path string; Err error }
```

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `// SetName sets the name.` | restates signature, no value | explain constraints, side effects, errors |
| `// TODO: improve this` with no owner/date | forever-todo | link to issue or remove |
| `// Note: this is fast.` | unsupported claim | benchmark in test, link to it |
| `// Deprecated.` (no body) | tooling reads the body | `// Deprecated: use NewFoo instead.` |
| Trailing period missing | inconsistent godoc layout | end every sentence with `.` |
| Multi-paragraph doc with no blank line | godoc collapses it | separate paragraphs with `//` blank lines |
| Documenting unexported names with godoc-style sentences | wastes effort; not rendered | one-line maintainer note is enough |
| Doc comment above the wrong symbol (blank line between) | godoc treats it as orphan | no blank line between comment and symbol |

## Verification Checklist

- [ ] `go doc ./...` renders cleanly for every package (no missing doc warnings from linters).
- [ ] Every exported identifier has a comment starting with its name.
- [ ] Every package has exactly one package comment.
- [ ] All deprecations include the `Deprecated:` paragraph.
- [ ] At least one `Example*` test per non-trivial exported function in a library package.
- [ ] README contains: title, summary, install, minimal working example, license.
- [ ] `golangci-lint run --enable=godot,revive` passes (revive's `exported` rule).

## Enforce With Linters

Mechanical checks belong to CI:

- `revive` `exported` — missing doc comments on exported names.
- `godot` — missing trailing periods in doc comments.
- `misspell` — typos in comments.
- `go vet` — basic format-string and other issues that also surface in docs.

## References

- [references/godoc-grammar.md](references/godoc-grammar.md) — sentence rules, headings, deprecation, formatting
- [references/examples-and-readme.md](references/examples-and-readme.md) — `Example*` tests, README outline, CONTRIBUTING/CHANGELOG
- [references/library-vs-application.md](references/library-vs-application.md) — what each project type needs, llms.txt, CLI `--help`

Referenced files: 4

go-error-handling6.76 KB

View saved version →

---
name: go-error-handling
description: "Use when writing, wrapping, inspecting, or logging Go errors. Covers strategy choice (sentinel vs typed vs opaque), wrapping with %w/%v, errors.Is/As/Join, the log-or-return rule, error strings, and panic/recover boundaries. Apply proactively whenever a function returns or accepts an error, even if the user has not asked about error handling."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.20+ for errors.Join. Wrapping (%w, errors.Is/As) requires Go 1.13+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Error Handling

Errors in Go are values. Treat them as part of the API: choose a strategy per failure mode, propagate with wrapping, inspect with `errors.Is`/`As`, and handle each error **exactly once**.

## Core Rules

1. **Errors are values, not exceptions.** Return them; do not panic across API boundaries.
2. **Handle each error exactly once.** *Either* log it *or* return it — never both.
3. **The caller decides what is exceptional.** Library code returns; binaries (or top-level handlers) decide whether to log, retry, or exit.
4. **Wrap only when you add real context.** A wrap that just repeats the function name is noise. Use `%w` to preserve identity; `%v` to deliberately hide an unstable type.

## Strategy Decision

Pick the simplest strategy that meets the caller's needs:

| Strategy | When to use | Example |
|---|---|---|
| **Opaque error** (default) | Caller only needs to know *something* failed | `errors.New("invalid input")` |
| **Sentinel error** | Caller needs to test for a specific named condition | `io.EOF`, `sql.ErrNoRows` |
| **Typed error** | Caller needs structured fields (path, code, retry-after) | `*os.PathError`, `*url.Error` |
| **Joined errors** | A single operation produced several independent failures | `errors.Join(errA, errB)` |

> Read [references/strategy-decision.md](references/strategy-decision.md) when the caller's needs are unclear or when migrating between strategies without breaking callers.

## Writing Errors

### Strings

- **Lowercase, no trailing punctuation.** Errors are composed: `fmt.Errorf("write %s: %w", path, err)` reads as one sentence.
- **Be specific.** `"open config: permission denied"` beats `"failed to open file"`.
- **Do not include the function name.** Stack context is added by wrapping at each layer.

### Creating

```go
// Opaque — the caller only checks != nil
return errors.New("invalid character in token")

// Sentinel — exported, package-level, named ErrXxx
var ErrNotFound = errors.New("user: not found")

// Typed — when callers need structured fields
type ValidationError struct {
    Field string
    Rule  string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s violates %s", e.Field, e.Rule)
}
```

## Wrapping and Inspection

### Wrap with `%w` to add context while preserving identity

```go
if err := db.Get(id); err != nil {
    return fmt.Errorf("loading user %d: %w", id, err)
}
```

### Inspect with `errors.Is` (identity) and `errors.As` (type)

```go
if errors.Is(err, sql.ErrNoRows) { /* handled */ }

var ve *ValidationError
if errors.As(err, &ve) {
    return reply.BadRequest(ve.Field)
}
```

**Never** compare error strings (`err.Error() == "..."`) — strings are not stable API.

### Join independent failures

```go
errs := errors.Join(
    validate(name),
    validate(email),
    validate(password),
)
if errs != nil {
    return errs // errors.Is/As walks both branches
}
```

> Read [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md) when deciding between `%w` (expose) and `%v` (hide), or when wrapping would leak an implementation detail.

## Error Flow

### Log or return — not both

```go
// Bad: caller will log it again, producing duplicate lines
if err := svc.Do(ctx); err != nil {
    slog.ErrorContext(ctx, "svc.Do failed", "err", err)
    return err
}

// Good: log only at the boundary that decides the request is done
if err := svc.Do(ctx); err != nil {
    return fmt.Errorf("doing svc work: %w", err)
}
```

The HTTP handler / job runner / `main` is the only layer that logs.

### Reduce nesting with guard clauses

```go
// Bad
if err == nil {
    if x, ok := f(); ok {
        return x, nil
    }
}
return zero, err

// Good
if err != nil {
    return zero, err
}
x, ok := f()
if !ok {
    return zero, errSomething
}
return x, nil
```

## Panic and Recover

`panic` is for **programmer errors** (impossible states) and **package initialization**. It is never the right way to return a normal failure.

```go
// Acceptable: invariants the type guarantees
func (q *Queue) MustEnqueue(v T) { if err := q.Enqueue(v); err != nil { panic(err) } }

// Acceptable: recover at the goroutine boundary so one bad request cannot kill the server
defer func() {
    if r := recover(); r != nil {
        log.Error("panic recovered", "value", r, "stack", debug.Stack())
        http.Error(w, "internal error", 500)
    }
}()
```

Do **not** use `recover` to convert panics into errors as normal control flow.

For custom error types, implement `Unwrap() error` (or `Unwrap() []error` in Go 1.20+) so `errors.Is`/`As` can reach the cause. See [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md#custom-unwrap).

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `return errors.New(err.Error())` | Drops identity; `errors.Is` breaks | `return fmt.Errorf("ctx: %w", err)` |
| `if err.Error() == "EOF"` | String matching against unstable text | `errors.Is(err, io.EOF)` |
| `_ = doThing()` | Silently swallows failures | Handle, log at boundary, or document why |
| Returning `*MyError` (concrete pointer) | Typed-nil trap; non-nil interface | Return `error` (see [references/typed-nil-trap.md](references/typed-nil-trap.md)) |
| Logging then returning the same error | Duplicate log lines, no single source of truth | Log only at the top boundary |
| Wrapping at every layer with no new info | `"a: b: c: d: real error"` chains | Drop the wrap and `return err` |

## Verification Checklist

Before finishing an error-handling change:

- [ ] No `err.Error()` string comparisons
- [ ] All wrapping uses `%w` (or `%v` is intentional and commented)
- [ ] Functions return the `error` interface, not concrete types
- [ ] Each error is logged at most once (at the request/job boundary)
- [ ] Sentinels are package-level `var ErrXxx = errors.New(...)`
- [ ] Typed errors expose only the fields callers actually need

## References

- [references/strategy-decision.md](references/strategy-decision.md) — picking opaque vs sentinel vs typed
- [references/wrapping-vs-shadowing.md](references/wrapping-vs-shadowing.md) — `%w` vs `%v` decisions
- [references/typed-nil-trap.md](references/typed-nil-trap.md) — why returning `*MyErr` breaks `== nil`

Referenced files: 4

go-functional-options6.08 KB

View saved version →

---
name: go-functional-options
description: "Use when designing a Go constructor or factory with 3+ optional parameters, or an API expected to grow new options over time. Covers the canonical Option interface pattern with unexported apply method, With* constructors, default values, and the interface-vs-closure tradeoff. Apply proactively when reviewing a New* function that takes many settings, even if the user didn't ask about functional options. Does not cover general function design (see go-functions)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Plain Go (any supported version)."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Functional Options

The functional options pattern lets a constructor stay backward compatible while accepting an open-ended set of optional settings. Callers pass only what differs from the defaults; new options never break old call sites.

## Core Rules

1. **Reach for functional options at 3+ optional parameters** or whenever the API will grow.
2. **The `options` struct is unexported.** Only the package owns its shape.
3. **The `Option` interface has an unexported `apply` method.** No external package can forge an option.
4. **Defaults go inside the constructor**, before options are applied.
5. **Required parameters stay positional;** only the optional ones go through `...Option`.
6. **Prefer the interface form over closures** — it composes better with testing, debugging, and `fmt.Stringer`.

## When to Use What

| Situation | Pattern |
|---|---|
| 0–2 optional params, stable API | Plain positional or named args |
| Config that callers usually pass whole | Config struct |
| 3+ optional params, growing API | **Functional options** |
| Mix of "must set together" + "rare overrides" | Config struct + small `Option` set |

> Read [references/options-vs-struct.md](references/options-vs-struct.md) when choosing between options and a plain config struct, or designing a hybrid.

## The Canonical Pattern

```go
package db

import "go.uber.org/zap"

// options is the package's private bag of settings.
type options struct {
    cache  bool
    logger *zap.Logger
}

// Option configures Open.
type Option interface {
    apply(*options)
}

// --- cacheOption -----------------------------------------------------------

type cacheOption bool

func (c cacheOption) apply(o *options) { o.cache = bool(c) }

// WithCache enables or disables the in-memory cache.
func WithCache(enabled bool) Option { return cacheOption(enabled) }

// --- loggerOption ----------------------------------------------------------

type loggerOption struct{ log *zap.Logger }

func (l loggerOption) apply(o *options) { o.logger = l.log }

// WithLogger sets the logger used by the connection.
func WithLogger(log *zap.Logger) Option { return loggerOption{log: log} }

// --- constructor -----------------------------------------------------------

// Open dials addr using the given options.
func Open(addr string, opts ...Option) (*Connection, error) {
    o := options{
        cache:  true,
        logger: zap.NewNop(),
    }
    for _, opt := range opts {
        opt.apply(&o)
    }
    // ... build the connection from o
    return &Connection{}, nil
}
```

### Caller Experience

```go
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false), db.WithLogger(log))
```

Compare to the alternative where all defaults must be repeated:

```go
db.Open(addr, db.DefaultCache, zap.NewNop()) // tedious
```

## Why an Interface, Not a Closure?

```go
// The closure variant — discouraged
type Option func(*options)
```

The interface form wins on:

1. **Testability** — option values can be compared in tests.
2. **Debuggability** — option types can implement `fmt.Stringer`.
3. **Documentation** — `godoc` lists each option type explicitly.
4. **Extensibility** — options can implement additional interfaces (e.g., `Validate()`).

Closures are shorter to write; they pay for that shortness in introspection.

## Defaults

Set defaults *before* applying options. A constructor that ignores its defaults is a bug magnet:

```go
o := options{
    cache:  true,
    logger: zap.NewNop(),
}
for _, opt := range opts {
    opt.apply(&o)
}
```

If a default needs computation (a temp dir, a process-wide ID), build it once during the constructor — not at package init.

## Quick Reference

```go
// 1. Unexported settings bag
type options struct { ... }

// 2. Exported interface, unexported method
type Option interface { apply(*options) }

// 3. One option type per setting
type widgetOption Widget
func (w widgetOption) apply(o *options) { o.widget = Widget(w) }
func WithWidget(w Widget) Option         { return widgetOption(w) }

// 4. Constructor: defaults, then apply
func New(required string, opts ...Option) (*Thing, error) {
    o := options{ /* defaults */ }
    for _, opt := range opts { opt.apply(&o) }
    return build(required, o)
}
```

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Exporting the `options` struct | External code mutates internals | Keep it unexported |
| `Option` with an **exported** `Apply` | Anyone can build an option | Unexported `apply` method |
| Applying options before defaults | Defaults overwrite caller intent | Defaults first, then `apply` |
| `func Option(*options)` closures | Opaque in tests/logs | Interface form |
| 7+ positional required params | Caller error-prone | Promote them into a config or options |
| Mixing required and optional through `...Option` | Required is no longer required | Keep required positional |

## Verification Checklist

- [ ] `options` is unexported
- [ ] `Option` interface has an unexported `apply(*options)` method
- [ ] Each setting has a `With*` constructor returning `Option`
- [ ] Constructor sets defaults first, then applies options
- [ ] Required parameters are not hidden behind `...Option`
- [ ] No exported `Apply` or `Option func(*options)` slipped in
- [ ] Doc comments explain each `With*` and its default

## References

- [references/options-vs-struct.md](references/options-vs-struct.md) — when to prefer a config struct, and how to combine the two

Referenced files: 3

go-functions5.91 KB

View saved version →

---
name: go-functions
description: "Use when organising functions in a Go file, formatting signatures, designing return values, or naming Printf-style helpers. Covers in-file ordering (type → ctor → exported → unexported → utils), multi-line signature shape, naked-parameter clarity, pointer-vs-value receivers, and the `f`-suffix rule. Apply proactively to any new function. Functional options: see go-functional-options."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Plain Go (any supported version)."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Function Design

A function's surface is read more often than its body. Optimize for the reader: predictable ordering in the file, signatures that scan, no hidden bool flags.

## Core Rules

1. **Order by use, not alphabet.** Types → constructors → exported methods → unexported → utilities.
2. **Keep signatures on one line when reasonable.** When wrapping, every parameter on its own line with a trailing comma.
3. **Never pass `*Interface`.** Pass the interface value; the underlying data can already be a pointer.
4. **Replace naked `bool`/`int` parameters with named types** or add `/* name */` comments at call sites.
5. **Printf-style functions end in `f`** so `go vet` can check the format.
6. **Prefer `%q` over `%s` plus manual quoting** when formatting strings for errors and logs.

## File Ordering

```go
type Server struct{ ... }

func NewServer(...) *Server { ... }     // constructor next to type

func (s *Server) Start(ctx context.Context) error { ... } // exported
func (s *Server) Stop() error           { ... }

func (s *Server) acceptLoop() { ... }   // unexported

func parseAddr(s string) (string, error) { ... } // file-local helper
```

Rules:

1. Types and their constructors sit together at the top.
2. Exported methods come before unexported ones.
3. File-local helpers go at the bottom.
4. Within a section, follow rough call order.

## Signature Formatting

```go
// Fits on one line — keep it on one line
func Sum(xs []int) int

// Too long — break with every param on its own line
func (r *Repo) SaveTransaction(
    ctx context.Context,
    userID string,
    tx Transaction,
    opts ...SaveOption,
) (string, error) {
    ...
}
```

The trailing comma is required and `gofmt`-stable.

### Avoid Naked Bool/Int Parameters

```go
// Bad — what does `true` mean?
NewServer(":8080", true, 30, false)

// Better — call-site comments
NewServer(":8080", true /* tls */, 30 /* maxConn */, false /* readonly */)

// Best — named types or options
NewServer(":8080", WithTLS(), WithMaxConn(30))
```

When a single bool is genuinely binary and obvious from the function name (`SetVerbose(true)`), it's fine.

> Read [references/signatures.md](references/signatures.md) for return-value styles, naked returns, function-as-parameter formatting, and the variadic-options call-site shape.

## Pointers to Interfaces

```go
// Bad
func process(r *io.Reader) { ... }

// Good
func process(r io.Reader) { ... }
```

An interface value already carries a pointer-sized data word. `*io.Reader` is a pointer to an interface — almost always a mistake.

## Printf and Stringer

Functions that accept a format string should end in `f`:

```go
func Logf(format string, args ...any)
```

`go vet` then checks that `%s`, `%d`, etc. match the argument types.

When formatting strings into errors or logs, prefer `%q`:

```go
return fmt.Errorf("unknown key %q", key) // unknown key "foo\nbar"
```

`%q` quotes and escapes; `%s` plus manual quoting (`"key \"" + key + "\""`) is fragile.

> Read [references/printf-and-stringer.md](references/printf-and-stringer.md) for `%v` vs `%s` vs `%q`, implementing `fmt.Stringer` safely, avoiding `String()` infinite recursion, and `fmt.Formatter`.

## Variadic Options at the Call Site

```go
db.Open(addr,
    db.WithCache(false),
    db.WithLogger(log),
    db.WithRetries(3),
)
```

Each option on its own line, trailing comma. Use this layout whenever the call doesn't fit on a single line.

## Constructors

A constructor immediately follows its type. Use the short form when no error is possible:

```go
type Counter struct{ n int }

func NewCounter() *Counter { return &Counter{} }
```

Return an error when construction can fail:

```go
func NewClient(addr string) (*Client, error) { ... }
```

Don't expose a half-built type through a constructor that "always succeeds" but requires `Init()` afterward.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Methods scattered randomly in the file | Hard to navigate | Group by type, exported-first |
| Five-argument wrapped signature with no trailing comma | `gofmt` keeps reformatting | Trailing comma |
| `func process(r *io.Reader)` | Pointer to interface | Pass `io.Reader` |
| `Log(msg string, format bool, ...)` | Combines two concerns; `vet` blind | Separate `Log` and `Logf` |
| `Open(":8080", true, false, 30)` | Unreadable booleans | Named options or `/* */` comments |
| `fmt.Errorf("got %s", key)` for arbitrary key | Special chars unclear in output | `%q` |
| Returning `*MyError` (concrete pointer) | Typed-nil interface trap | Return `error` |

## Verification Checklist

- [ ] Types appear above their constructors; exported methods above unexported
- [ ] Long signatures wrap with one parameter per line and a trailing comma
- [ ] No pointer-to-interface parameters
- [ ] Bool/int parameters are either obvious from the function name or named with `/* */` comments
- [ ] Functions taking a format string end in `f`
- [ ] Errors and logs use `%q` when formatting arbitrary strings
- [ ] Constructors return `(*T, error)` when construction can fail — no half-built objects

## References

- [references/signatures.md](references/signatures.md) — multi-line wrapping, named results, function-typed parameters
- [references/printf-and-stringer.md](references/printf-and-stringer.md) — format verbs, `fmt.Stringer`, recursion traps, `fmt.Formatter`

Referenced files: 3

go-generics6.05 KB

View saved version →

---
name: go-generics
description: "Use when deciding whether to introduce Go generics, writing generic functions or types, composing type constraints, or choosing between type aliases and type definitions. Apply proactively when a user is writing a utility function that could conceivably work with multiple types, even if they didn't mention generics. Does not cover interface-only designs (see go-interfaces)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Generics require Go 1.18+; `cmp.Ordered` requires Go 1.21+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Generics

Generics are a powerful but easy-to-misuse feature. The Go answer is pragmatic: write concrete code first, then generalize only when you have a real second caller.

## Core Rules

1. **Write concrete first.** Reach for generics only when a second type actually needs the same logic.
2. **If an interface already models the behavior, use the interface.** Don't pile type parameters on top.
3. **Prefer standard constraints** (`comparable`, `cmp.Ordered`, `any`) over hand-rolled unions.
4. **Don't over-constrain.** `comparable` is usually enough; the narrower the constraint, the fewer callers benefit.
5. **Name type parameters with a single uppercase letter** (`T`, `K`, `V`, `E`) unless a longer name genuinely helps.
6. **Don't use generics for interface satisfaction.** `func F[T io.Reader](r T)` is just `func F(r io.Reader)`.
7. **Don't wrap stdlib containers** "for generic convenience" unless you eliminate real duplication.

## Decision Flow

```
Multiple types need the same logic?
├─ No  → concrete type
├─ Yes → do they share a useful interface?
│        ├─ Yes → use the interface
│        └─ No  → use generics
```

## When NOT to Use Generics

```go
// Premature: only ever called with int
func Sum[T constraints.Integer | constraints.Float](xs []T) T {
    var t T
    for _, x := range xs { t += x }
    return t
}

// Better
func SumInts(xs []int) int {
    var t int
    for _, x := range xs { t += x }
    return t
}
```

> "Write code, don't design types." — Griesemer & Taylor

## When Generics Pay Off

- A library function the standard library would have written generically: `slices.Index`, `maps.Keys`, `slices.SortFunc`.
- Concurrent-safe data structures (typed sets, ordered maps) where boxing into `any` would be both ugly and slow.
- Map/Reduce-style helpers that genuinely apply to many element types.

## Type Parameter Naming

| Name | Typical use |
|---|---|
| `T` | General element / first type |
| `K` | Map key |
| `V` | Map value |
| `E` | Element of a collection |
| `R` | Result of a transform |

Multi-letter names are reserved for constraints where the meaning is non-obvious:

```go
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
```

## Constraint Composition

```go
type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64
}

func Sum[T Numeric](xs []T) T {
    var t T
    for _, x := range xs { t += x }
    return t
}
```

- `~int` means "anything whose underlying type is `int`" — covers `type Celsius int`.
- `|` unions widen the set.
- Prefer `cmp.Ordered` (Go 1.21+) over rolling your own.

> Read [references/constraints.md](references/constraints.md) for the constraint catalogue, when `~` matters, and how type inference interacts with constraints.

## Common Pitfalls

### Don't Wrap Stdlib Types Generically

```go
// Adds complexity, eliminates no duplication
type Set[T comparable] struct {
    m map[T]struct{}
}

// Use the builtin
seen := map[string]struct{}{}
seen["a"] = struct{}{}
```

A generic wrapper around `map[T]struct{}` is only worth it if you keep it for many call sites *and* provide methods that pay for the indirection (e.g., `Union`, `Intersect`).

### Don't Use Generics for Interface Satisfaction

```go
// Pointless type parameter
func Process[T io.Reader](r T) error { ... }

// Just use the interface
func Process(r io.Reader) error { ... }
```

### Don't Over-Constrain

```go
// Restrictive without reason
func Contains[T interface{ ~int | ~string }](xs []T, t T) bool { ... }

// comparable is enough
func Contains[T comparable](xs []T, t T) bool { ... }
```

> Read [references/generics-vs-interfaces.md](references/generics-vs-interfaces.md) when interfaces and generics both seem to fit, and you have to choose.

## Type Aliases vs Definitions

```go
type Old = pkg.New  // alias: same type, alternate name
type Old pkg.New    // definition: new type, fresh method set
```

Type aliases (`=`) are for **package migrations** and gradual API moves. For new types, use a definition.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Generic for a single instantiation | Indirection without payoff | Concrete code |
| Generic where an interface fits | Type parameter is just `io.Reader` in disguise | Accept the interface |
| `interface{ ~int }` when `comparable` suffices | Restricts callers, no benefit | Loosen the constraint |
| Custom `Numeric` constraint | `cmp.Ordered` exists | Standard constraint |
| `Set[T]` wrapper around `map[T]struct{}` | Two-line struct, no methods | Use the map directly |
| Generic function with two type params, neither used | The compiler can infer nothing | Drop one or both |

## Verification Checklist

- [ ] At least two real, current call sites benefit from the type parameter
- [ ] An interface would not be a simpler model
- [ ] Constraint is the loosest one that compiles (`any`, `comparable`, `cmp.Ordered` preferred)
- [ ] Type parameter names are conventional letters unless clarity demands more
- [ ] No `T` exists only to satisfy an interface — accept the interface instead
- [ ] No generic wrapper added without methods that justify it
- [ ] Doc comment explains what the type parameter must support

## References

- [references/constraints.md](references/constraints.md) — constraint catalogue, `~` and `|`, `cmp.Ordered`, type inference
- [references/generics-vs-interfaces.md](references/generics-vs-interfaces.md) — picking between a generic and an interface

Referenced files: 3

go-graphql8.5 KB

View saved version →

---
name: go-graphql
description: "Use when building or reviewing a GraphQL API in Go. Covers library choice (gqlgen vs graph-gophers), schema design (nullability, pagination, mutation envelopes), thin resolver pattern, per-request DataLoaders for N+1, authentication via context plus schema directives, error presenters, subscription lifecycle (context cancellation), and production hardening (complexity limits, introspection gating). Apply when working with github.com/99designs/gqlgen or github.com/graph-gophers/graphql-go."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+. gqlgen v0.17+ or graph-gophers/graphql-go v1.5+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go GraphQL

Both production-grade Go GraphQL libraries are schema-first: write SDL (`.graphql`), bind Go resolvers. Pick the library, write the schema deliberately, and treat DataLoaders + complexity limits as non-optional.

## Core Rules

1. **Schema is the contract.** Design nullability and pagination once; clients depend on it forever. A change from nullable to non-null is a breaking change.
2. **Resolvers are thin.** Translate GraphQL input → domain call → GraphQL output. No SQL, no business logic.
3. **DataLoaders are per-request.** Construct in HTTP middleware, stash in `context`. A package-level DataLoader is a cross-tenant data leak.
4. **Authenticate in middleware, authorize in the schema.** HTTP middleware extracts identity; schema directives (or resolver checks) enforce per-field rules.
5. **Subscriptions respect context.** Every subscription goroutine selects on `ctx.Done()` and `defer close(ch)`. Otherwise a disconnected client leaks a goroutine forever.
6. **Production limits are non-optional.** Set complexity caps; gate introspection by environment; never expose raw internal errors.

## Library Decision

| Library | Approach | Type safety | Build step | Pick when |
|---|---|---|---|---|
| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, Federation, strict types |
| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Small/medium schemas, simple pipeline |
| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** — verbose, no SDL |

> Read [references/gqlgen.md](references/gqlgen.md) for the codegen workflow, `gqlgen.yml`, DataLoaders, and Federation.
> Read [references/graph-gophers.md](references/graph-gophers.md) for the reflection model, type mapping, and tracing.

## Schema Design

```graphql
type User {
  id: ID!                # opaque scalar; never expose Int
  email: String!         # server can always return this → non-null
  bio: String            # may be unset → nullable
  posts(first: Int = 10, after: String): PostConnection!
}

type CreateUserPayload {  # mutation envelope: business errors as data
  user: User
  errors: [UserError!]!
}

type PostConnection {     # Relay cursor pagination
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}
```

**Nullability rule.** A field is `!` only when the server can *always* return a value. A resolver error on a non-null field nulls the parent object — cascade failures. Nullable fields null only themselves.

**Pagination.** Cursor connections beat offset pagination on large or write-heavy datasets — cursors are stable under concurrent inserts.

**Mutation envelopes.** Wrap mutation results so business-level errors (validation, conflict) become first-class data instead of polluting the top-level `errors` array.

## Thin Resolvers

```go
// Good — resolver translates and delegates.
func (r *mutationResolver) CreateUser(ctx context.Context, in CreateUserInput) (*CreateUserPayload, error) {
    user, err := r.users.Create(ctx, in.Email, in.Name)
    if err != nil {
        return nil, presentError(err)
    }
    return &CreateUserPayload{User: toGQLUser(user)}, nil
}

// Bad — SQL inside the resolver.
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    // ...
}
```

Use per-type resolver structs (`userResolver`, `postResolver`) instead of one monolithic resolver. It scales with the schema.

## N+1 Prevention with DataLoaders

A naive `User.posts` resolver fires one SQL query per user — O(n) round-trips. DataLoaders coalesce per-field loads within a single tick into one batched query.

```go
// Good — per-request DataLoader in middleware.
func DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        loaders := &Loaders{
            PostsByUser: newPostsByUserLoader(r.Context(), db),
        }
        ctx := context.WithValue(r.Context(), loadersKey{}, loaders)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Bad — package-level DataLoader caches across requests.
var globalLoader = newPostsByUserLoader(context.Background(), db)
```

Package-level DataLoaders silently serve user A's data to user B's request as long as the cached key matches. This is the most dangerous bug in Go GraphQL services.

## Authn vs Authz

Authenticate in HTTP middleware (extract identity, stash in `ctx`); authorize per-field via schema directives (`@hasRole(role: ADMIN)`) in gqlgen, or resolver-level checks in graph-gophers. Authorization policy belongs in the schema, not scattered across resolvers. See [references/gqlgen.md](references/gqlgen.md).

## Error Handling

Never surface raw `error` values — they leak SQL fragments and internals. Install an `ErrorPresenter` (gqlgen) or implement `ResolverError` (graph-gophers) that returns sanitized messages. Attach a stable `code` in extensions (`NOT_FOUND`, `FORBIDDEN`) for client handling. Use `graphql.AddError(ctx, err)` for non-fatal field errors with partial data.

## Subscriptions

Every subscription goroutine must `defer close(ch)` and select on `ctx.Done()` in both the receive and send branches:

```go
go func() {
    defer close(ch)
    for {
        select {
        case <-ctx.Done(): return
        case msg := <-sub:
            select { case ch <- msg: case <-ctx.Done(): return }
        }
    }
}()
```

Without this, every disconnected client leaks a goroutine.

## Production Hardening

- `extension.FixedComplexityLimit(200)` (gqlgen) or `graphql.MaxDepth(10)` + `MaxParallelism(10)` (graph-gophers)
- Gate introspection behind an env check
- Consider persisted queries (gqlgen APQ) so production only accepts pre-approved hashed queries

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Package-level DataLoader | Cross-tenant data leakage, stale cache | Construct per-request in middleware |
| SQL in resolver | Resolver becomes data layer; no batching | Delegate to service; load via DataLoader |
| Non-null field that can fail | Cascade-nulls the parent | Make it nullable; or guarantee in resolver |
| Editing `models_gen.go` | Wiped on next codegen | Use `autobind` / `models.<T>.model` in gqlgen.yml |
| Introspection in production | Exposes full schema surface | Gate by env |
| Subscription goroutine leak | Each disconnect leaks a goroutine | `defer close(ch)` + `select ctx.Done()` |
| No complexity cap | Single deep query = CPU/memory DoS | `FixedComplexityLimit(N)` or persisted queries |
| Raw internal error to client | Leaks DB messages, stack traces | `ErrorPresenter` returning sanitized message |
| `int` field for `Int!` in graph-gophers | Library expects `int32` | Use `int32` (or `float64` for `Float`) |

## Verification Checklist

- [ ] Every non-null field is one the server can always return
- [ ] List fields use cursor pagination, not offset
- [ ] Mutations return envelope types with `errors: [UserError!]!`
- [ ] DataLoaders are constructed in HTTP middleware, never package-level
- [ ] Authentication is in HTTP middleware; authorization is in directives or resolver checks
- [ ] `ErrorPresenter` (gqlgen) or `ResolverError` (graph-gophers) sanitizes internals
- [ ] Every subscription `defer close(ch)` and selects on `ctx.Done()`
- [ ] Complexity limit set; introspection gated by env
- [ ] No resolver reads SQL directly

## References

- [references/gqlgen.md](references/gqlgen.md) — codegen workflow, `gqlgen.yml`, DataLoaders, Federation
- [references/graph-gophers.md](references/graph-gophers.md) — reflection model, type mapping, tracing
- [references/dataloaders.md](references/dataloaders.md) — batched loading patterns, cache lifecycle, gotchas
- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each anti-pattern

Referenced files: 5

go-grpc8.42 KB

View saved version →

---
name: go-grpc
description: "Use when implementing or reviewing gRPC servers/clients in Go. Covers .proto organisation, code generation with protoc/buf, server bootstrap (interceptors, health, graceful shutdown), client patterns (reuse, deadlines, retries), status.Code error handling, streaming, TLS/mTLS, and bufconn testing. Apply when writing .proto files, adding interceptors, or auditing a service for production readiness."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+, protoc (or buf), and google.golang.org/grpc v1.60+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go gRPC

Treat gRPC as a transport. Keep `.proto`-generated code and business logic separated. The official Go implementation is `google.golang.org/grpc`; pair it with `protoc-gen-go` + `protoc-gen-go-grpc` (or `buf generate`).

## Core Rules

1. **One concern per layer.** `.proto` defines the contract; generated code lives in `gen/`; service implementation lives in `internal/`. Never edit generated files.
2. **Always wrap RPC arguments in Request/Response messages.** Bare scalars (`string`, `int32`) cannot be evolved without breaking callers.
3. **Return typed status codes, never raw errors.** A `fmt.Errorf` becomes `codes.Unknown` on the wire — the client cannot decide whether to retry.
4. **Every client call has a deadline.** No `context.Background()` to a remote service. Set `context.WithTimeout` per call.
5. **Reuse connections.** HTTP/2 multiplexes; creating a new `grpc.ClientConn` per request is a TLS handshake leak.
6. **Disable reflection in production.** Reflection is a developer convenience that doubles as an API enumeration tool for attackers.

## When to Use What

| Need | Use |
|---|---|
| Define service | `.proto` file in `proto/<service>/v1/` |
| Generate stubs | `buf generate` or `protoc --go_out --go-grpc_out` |
| Cross-cutting (auth, logging, recovery) | `grpc.ChainUnaryInterceptor` / `ChainStreamInterceptor` |
| Health probes (Kubernetes) | `grpc_health_v1` from `google.golang.org/grpc/health` |
| Errors with details | `status.Errorf(codes.X, ...)` + `WithDetails(errdetails.BadRequest{...})` |
| Tests | `google.golang.org/grpc/test/bufconn` |
| Service mesh / mTLS | `credentials.NewTLS` or delegate to Istio/Linkerd |

> Read [references/proto-and-codegen.md](references/proto-and-codegen.md) when organizing `.proto` packages or wiring `buf`.
> Read [references/status-and-errors.md](references/status-and-errors.md) when mapping domain errors to gRPC codes.

## Server Bootstrap

```go
import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(recoveryUnary, loggingUnary, authUnary),
    grpc.ChainStreamInterceptor(recoveryStream, loggingStream),
)
pb.RegisterUserServiceServer(srv, &userService{...})
healthpb.RegisterHealthServer(srv, health.NewServer())

go func() { _ = srv.Serve(lis) }()

// Graceful shutdown bounded by a hard timeout.
<-shutdownSignal
stopped := make(chan struct{})
go func() { srv.GracefulStop(); close(stopped) }()
select {
case <-stopped:
case <-time.After(15 * time.Second):
    srv.Stop()
}
```

Three pieces are non-negotiable: interceptors for cross-cutting concerns, health service for Kubernetes probes, and a bounded graceful shutdown.

## Client Bootstrap

```go
conn, _ := grpc.NewClient("dns:///user-service:50051",
    grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)),
    grpc.WithDefaultServiceConfig(`{
      "loadBalancingPolicy": "round_robin",
      "methodConfig": [{
        "name": [{"service": "user.v1.UserService"}],
        "timeout": "5s",
        "retryPolicy": {
          "maxAttempts": 3, "initialBackoff": "0.1s", "maxBackoff": "1s",
          "backoffMultiplier": 2, "retryableStatusCodes": ["UNAVAILABLE"]
        }
      }]
    }`),
)
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second); defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: id})
```

The service config is the right place for retries — let the library handle the loop, backoff, and `UNAVAILABLE`-only filter.

## Errors

A raw Go error returned from an RPC becomes `codes.Unknown`. The client cannot tell a 404 from a 500. Always use `status.Errorf`:

```go
if errors.Is(err, ErrNotFound) {
    return nil, status.Errorf(codes.NotFound, "user %q not found", req.Id)
}
if errors.As(err, &validationErr) {
    st, _ := status.New(codes.InvalidArgument, "validation").WithDetails(
        &errdetails.BadRequest{FieldViolations: violations(validationErr)},
    )
    return nil, st.Err()
}
return nil, status.Errorf(codes.Internal, "lookup: %v", err)
```

Quick map:

| Domain | Code |
|---|---|
| Missing/invalid field | `InvalidArgument` |
| Not found | `NotFound` |
| Already exists | `AlreadyExists` |
| Unauthenticated | `Unauthenticated` |
| Authenticated but forbidden | `PermissionDenied` |
| Rate-limited | `ResourceExhausted` |
| Dependency down, retriable | `Unavailable` |
| Bug, unexpected | `Internal` |

## Streaming

| Pattern | Use case |
|---|---|
| Server streaming | Log tailing, paginated result sets, server-sent events |
| Client streaming | File upload, batch ingest |
| Bidirectional | Chat, real-time sync |

Streams must respect `ctx.Done()`. A goroutine reading from a stream after the client disconnects is a slow leak.

```go
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    for _, u := range s.repo.All(stream.Context()) {
        if err := stream.Send(toProto(u)); err != nil {
            return err // includes ctx canceled
        }
    }
    return nil
}
```

## Testing with bufconn

`bufconn` is an in-memory `net.Listener`. It exercises the real gRPC stack — interceptors, marshaling, metadata — without binding a TCP port. See [references/testing.md](references/testing.md) for the full harness plus table-driven status-code assertions, metadata injection, and stream testing.

## Security Notes

- TLS in production. Plaintext is only acceptable behind a confirmed-private network (and even then mTLS is preferable).
- For service-to-service auth, prefer a mesh (Istio/Linkerd) over hand-rolled token validation.
- For user auth, implement `credentials.PerRPCCredentials` to attach a token and validate inside an auth interceptor.
- Reflection: enable in dev, disable in prod via build tag or env flag.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `return fmt.Errorf("not found")` | Wire code is `Unknown`, clients can't retry-discriminate | `status.Errorf(codes.NotFound, ...)` |
| `context.Background()` to a client call | No deadline → goroutines pile up on a slow dependency | `context.WithTimeout(parent, 5s)` |
| New `ClientConn` per request | TLS handshake every call; sockets exhaust | One `grpc.NewClient` at startup, reuse |
| Bare `string` as RPC argument | Cannot add fields without breaking callers | Always Request/Response messages |
| Reflection on in production | Lets attackers enumerate every method | Compile-out with build tag in prod |
| `codes.Internal` for all errors | Client retry config can't distinguish bugs from outages | Map domain → specific codes |
| No health service | Kubernetes can't gate traffic; rolling deploys break | Register `grpc_health_v1` |
| Ignoring `stream.Context().Done()` | Goroutines run after client disconnect | Select on `ctx.Done()` in stream loops |

## Verification Checklist

- [ ] `.proto` packages are versioned (`pkg/v1`, not `pkg`)
- [ ] All RPCs take Request and return Response messages
- [ ] Generated code is in a separate directory, never edited
- [ ] Every error return uses `status.Errorf` with a specific code
- [ ] Every client call has a deadline via `context.WithTimeout`
- [ ] Server registers `grpc_health_v1`
- [ ] `GracefulStop` is bounded by a `time.After` fallback
- [ ] Reflection is gated to non-production builds
- [ ] Tests use `bufconn` and assert `status.Code(err)`

## References

- [references/proto-and-codegen.md](references/proto-and-codegen.md) — `.proto` layout, `buf.yaml`, codegen flags
- [references/status-and-errors.md](references/status-and-errors.md) — code mapping, rich details with `errdetails`
- [references/testing.md](references/testing.md) — `bufconn`, metadata, streaming assertions
- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each anti-pattern

Referenced files: 5

go-interfaces8.71 KB

View saved version →

---
name: go-interfaces
description: "Use when defining or implementing Go interfaces, composing types through embedding, designing dependency-injection seams, or deciding between pointer and value receivers. Apply proactively whenever a new abstraction is introduced or a constructor returns an abstract type, even if the user has not asked about interfaces. Does not cover generics (see go-generics)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Targets Go 1.21+. Generics guidance is delegated to go-generics."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Interfaces and Composition

Interfaces in Go are *consumer contracts*, not implementation hierarchies. They should be **small**, **discovered late**, and **owned by the package that uses them** — not the package that satisfies them.

## Core Rules

1. **Accept interfaces, return concrete types.** Consumers state what they need; producers expose what they have.
2. **Interfaces belong in the consumer package.** Defining an interface next to its sole implementation is almost always wrong.
3. **Don't design with interfaces — discover them.** Wait for a second implementation or a test mock to demand one.
4. **The bigger the interface, the weaker the abstraction.** Aim for 1–3 methods; compose larger contracts from smaller ones.
5. **Receiver consistency:** if any method needs a pointer receiver, give *every* method a pointer receiver.
6. **Verify satisfaction at compile time** with `var _ I = (*T)(nil)` when the relationship must not break silently.
7. **Use the comma-ok idiom for every type assertion.** A bare assertion panics on mismatch.

## Decision: Should I Introduce an Interface?

| Situation | Verdict |
|---|---|
| Single implementation, no tests need to swap it | No interface. Use the concrete type. |
| Second implementation appears (or is imminent) | Extract an interface in the consumer package. |
| Test needs to fake an external dependency | Define a small interface in the consumer; pass a fake. |
| You want to expose optional behaviour (`Flusher`, `ReaderFrom`) | Define a tiny interface; check with `_, ok := v.(Iface)`. |
| You want a stable plugin/SPI boundary | Yes, but keep it minimal and version it explicitly. |

> Read [references/consumer-owned-interfaces.md](references/consumer-owned-interfaces.md) when migrating a producer-defined interface back to the consumer, or when designing a new package boundary.

## Accept Interfaces, Return Concrete Types

```go
// Good — consumer defines what it needs
package notify
type Sender interface { Send(to, body string) error }
type Service struct{ s Sender }
func NewService(s Sender) *Service { return &Service{s: s} }

// Good — producer returns a concrete type
package email
type Client struct{ /* ... */ }
func New(cfg Config) *Client { /* ... */ }
func (c *Client) Send(to, body string) error { /* ... */ }
```

```go
// Bad — producer defines and returns its own interface,
// forcing every consumer to depend on email.Sender.
func New(cfg Config) Sender { return &client{...} }
```

The exception is "expose an interface, hide the implementation": when a type has no exported methods beyond what the interface promises, returning the interface (`func NewHash() hash.Hash32`) is fine.

## Keep Interfaces Small

Standard library interfaces are the model: `io.Reader`, `io.Writer`, `io.Closer`, `fmt.Stringer`, `error` — one or two methods each. Compose larger contracts:

```go
type ReadWriteCloser interface { io.Reader; io.Writer; io.Closer }
```

If you find yourself writing a five-method interface, split it until each piece has a single reason to exist.

## Compile-Time Satisfaction Check

```go
var _ io.ReadWriter = (*MyBuffer)(nil)
```

Use when the type must satisfy an interface for correctness (custom JSON marshalling, `http.Handler`) and no other static use already enforces it. Don't add one for every interface.

## Type Assertions and Type Switches

Always use the comma-ok form. Type switches re-declare the variable; cases with multiple types fall back to the interface type. Use optional-behaviour assertions to *enhance* a path without requiring the capability:

```go
s, ok := v.(string)                              // comma-ok
switch x := v.(type) { case string: /* ... */ }  // type switch
if f, ok := w.(http.Flusher); ok { f.Flush() }   // optional behaviour
```

## Embedding: Composition, Not Inheritance

Struct embedding promotes the inner type's methods and fields to the outer type. Use it deliberately — every promoted method becomes part of your public API.

```go
type Server struct {
    *slog.Logger          // exposes Info/Warn/Error on Server
    addr string
}
```

| Use embedding when | Use a named field when |
|---|---|
| You want the outer type to *be* an enhanced version of the inner | You only need the inner type internally |
| The full inner API should be promoted | You want to delegate explicitly to a subset |

Avoid embedding in exported types unless the promotion is the whole point. The inner type's method set is locked in once published.

> Read [references/embedding-and-receivers.md](references/embedding-and-receivers.md) when designing struct embedding, overriding promoted methods, resolving name conflicts, or choosing between pointer and value receivers.

## Dependency Injection via Interfaces

Constructors take interfaces; tests pass fakes. No DI container required.

```go
type UserStore interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

type UserService struct{ store UserStore }
func NewUserService(s UserStore) *UserService { return &UserService{store: s} }
```

The `UserStore` interface lives in the package that defines `UserService`. The concrete `*pgUserStore` lives in a database package and doesn't know `UserService` exists.

## Preventing Accidental Copies

Structs that must not be copied (those holding a mutex, internal pointers, or a `sync.WaitGroup`) should embed a `noCopy` sentinel so `go vet` catches the mistake:

```go
type noCopy struct{}
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type ConnPool struct {
    _   noCopy
    mu  sync.Mutex
    /* ... */
}
```

Pass these by pointer (`func process(p *ConnPool)`), never by value.

**Don't reach for `noCopy` reflexively.** Plain value types (config structs, request DTOs, immutable snapshots) *should* be copyable — adding `noCopy` to them locks consumers into pointer-only APIs for no gain. The rule of thumb: if the struct owns a `sync.Mutex`, `sync.WaitGroup`, `sync.Pool`, internal `chan`, or a pointer that *must* stay unique (file handle, OS resource), embed `noCopy`. Otherwise leave it copyable.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Producer-defined interface returned from constructor | Couples every consumer to the producer's package | Return the concrete type; let consumers define interfaces |
| Five-plus method interface | Hard to implement, hard to mock | Split into small interfaces; compose |
| Premature interface with one implementation | Indirection without value | Start concrete; extract when a second consumer appears |
| `v := x.(T)` without `ok` | Panics on mismatch | `v, ok := x.(T)` |
| Embedding a concrete type into an exported struct | Inner API leaks into your public surface | Use a named, unexported field |
| Mixing pointer and value receivers on one type | `(*T)` and `T` have different method sets — confusing satisfaction errors | Pick one receiver style for the whole type |
| `ToString()` / `ReadData()` instead of canonical names | Breaks `fmt.Stringer` / `io.Reader` discovery | Honour `String()` / `Read(p []byte) (int, error)` |
| Returning `*MyErr` instead of `error` | Typed-nil trap; `err != nil` is true even when "no error" | Return the interface type |

## Verification Checklist

Before finishing an interface change:

- [ ] Interfaces are defined in the package that consumes them
- [ ] Constructors return concrete types (or hide a single unexported implementation behind a small interface)
- [ ] No interface has more than ~3 methods unless it composes named smaller ones
- [ ] Every type assertion uses the comma-ok form
- [ ] Pointer vs value receivers are consistent across all methods on a type
- [ ] Compile-time `var _ I = (*T)(nil)` exists where silent regressions would hurt
- [ ] Exported structs don't accidentally promote inner-type APIs through embedding

## References

- [references/consumer-owned-interfaces.md](references/consumer-owned-interfaces.md) — where interfaces live and how to migrate
- [references/embedding-and-receivers.md](references/embedding-and-receivers.md) — embedding, overrides, pointer vs value receivers
- [references/std-interfaces-cheatsheet.md](references/std-interfaces-cheatsheet.md) — canonical signatures from the standard library

Referenced files: 4

go-linting7.41 KB

View saved version →

---
name: go-linting
description: "Use when setting up linting for a Go project, configuring golangci-lint, picking a linter set, suppressing findings with //nolint, or wiring lint checks into CI. Apply proactively whenever a project lacks .golangci.yml, when lint output is unclear, or when a new package needs the project's quality bar. Does not cover code review process (see go-code-review)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Targets golangci-lint v2.x and Go 1.21+. Some commands (golangci-lint fmt) require v2."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Linting

The single most important property of a linting setup is **consistency**: every contributor and every CI run uses the same rules. `golangci-lint` is the tool; a checked-in `.golangci.yml` is the contract.

## Core Rules

1. **Every Go project has a `.golangci.yml`** at the repository root. It is the source of truth for which linters run.
2. **Lint runs in CI on every PR.** A green build means lint is green.
3. **Lint runs locally before commit.** A pre-commit hook or `make lint` keeps the feedback loop fast.
4. **Suppress with reasons.** `//nolint:linter // why` — never bare `//nolint`.
5. **Fix the cause first.** A suppression should be the last resort, not the default reaction.
6. **Never silence security linters** (`gosec`, `bodyclose`, `sqlclosecheck`) without a strong, documented reason.

## Setup Procedure

1. Install: `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest` (or `brew install golangci-lint`).
2. Drop a baseline [`.golangci.yml`](assets/.golangci.yml) at the repo root.
3. Run `golangci-lint run ./...`.
4. Fix the findings in order — formatting first, `govet` next, style last.
5. Re-run until clean. Commit `.golangci.yml` and any source fixes together.
6. Add the CI workflow (see [references/ci-integration.md](references/ci-integration.md)).

## Minimum Linter Set

These five catch the most common issues and have the lowest noise rate. Start here:

| Linter | Catches |
|---|---|
| `errcheck` | Unchecked error returns |
| `govet` | Mistakes that compile but are wrong (printf args, shifts, etc.) |
| `staticcheck` | Bug-prone patterns, dead code, simplifications |
| `ineffassign` | Assignments whose value is never read |
| `revive` | Style issues (modern replacement for `golint`) |

Add formatting on top: `gofmt` / `goimports` (or `gofumpt` for stricter rules). With golangci-lint v2 these run via `golangci-lint fmt`.

## Recommended Additional Linters

Enable these once the baseline is clean:

| Linter | When to enable |
|---|---|
| `gosec` | Any service that handles untrusted input |
| `bodyclose` | Any code that calls `http.Client.Do` |
| `sqlclosecheck` | Any code using `database/sql` |
| `nilerr` | Any code that does `if err != nil { return nil }` style returns |
| `misspell` | Always — comments and strings |
| `unconvert` | Always — flags useless type conversions |
| `nolintlint` | Always — enforces the `//nolint` rules below |
| `paralleltest` | If most tests can use `t.Parallel()` |
| `thelper` | If you write test helpers (enforces `t.Helper()`) |
| `testifylint` | If the project uses `testify` |
| `gocyclo` / `gocognit` | When you want a complexity ceiling |
| `exhaustive` | When you use `iota`-based enums and want full `switch` coverage |

> Read [references/linter-catalog.md](references/linter-catalog.md) when picking from the long tail of correctness, style, security, and complexity linters, or when deciding which ones to enable on legacy code.

## Development Workflow

```makefile
lint:
	golangci-lint run ./...

lint-fix:
	golangci-lint run --fix ./...

fmt:
	golangci-lint fmt ./...

ci-lint:
	golangci-lint run --new-from-rev=origin/main ./...
```

| Task | Command |
|---|---|
| Run all enabled linters | `golangci-lint run ./...` |
| Auto-fix everything fixable | `golangci-lint run --fix ./...` |
| Format the tree (v2+) | `golangci-lint fmt ./...` |
| Lint only changed code | `golangci-lint run --new-from-rev=origin/main ./...` |
| Run one linter | `golangci-lint run --enable-only=govet ./...` |
| Show which linters exist | `golangci-lint linters` |

`--new-from-rev` is what makes incremental adoption work: legacy code stays untouched, new and changed code must meet the bar.

> Read [references/ci-integration.md](references/ci-integration.md) when wiring GitHub Actions, pre-commit hooks, or selective linting on PRs.

## Suppressing Findings

```go
// Good: specific linter + a reason
//nolint:errcheck // fire-and-forget; Sync error is not actionable on shutdown
_ = logger.Sync()
```

```go
// Bad: blanket, no reason — nolintlint will flag this
//nolint
_ = logger.Sync()
```

Rules (enforced by `nolintlint`):

- Name the linter: `//nolint:errcheck`, not `//nolint`.
- Include a justification after `//`.
- Place the directive on the same line as the finding, or immediately above the construct it applies to.
- Prefer per-line suppressions over file-level `//nolint:all`.

> Read [references/nolint-directives.md](references/nolint-directives.md) when deciding between inline, block, and file-scope suppressions, or when reviewing existing `//nolint` for stale rationale.

## Interpreting Output

Each finding looks like:

```
path/to/file.go:42:10: message describing the issue (linter-name)
```

The linter name in parentheses is the key — look it up in the catalog to see what it actually checks, then either fix the code or suppress with a reason that names the same linter.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| No `.golangci.yml` in the repo | Each contributor lints differently or not at all | Commit a baseline config; CI enforces it |
| `//nolint` with no linter name | Disables every check on that line, silently | `//nolint:errcheck // reason` |
| `//nolint:all` at the top of a file | Whole file escapes review | Suppress per construct with a reason |
| Lint failures non-blocking in CI | "Green build" becomes meaningless | Block merges on lint failure |
| Enabling 100 linters on day one | Noise drowns signal; team gives up | Start with the minimum set, add gradually |
| Suppressing `gosec` / `bodyclose` without justification | Silently hides real bugs | Fix the cause; if you can't, document why in the suppression |
| Different lint versions in dev vs CI | "Works on my machine" comes back | Pin the version in CI and document it in the README |
| Linting after the fact, only in CI | Slow iteration; PRs ping-pong | Run locally via `make lint` or a pre-commit hook |

## Verification Checklist

Before merging a linting change:

- [ ] `.golangci.yml` exists at the repo root and is checked in
- [ ] `golangci-lint run ./...` is clean (or `--new-from-rev` reports no new issues)
- [ ] CI runs `golangci-lint` and blocks merges on failure
- [ ] `nolintlint` is enabled; no bare `//nolint` remains
- [ ] Every `//nolint` includes a linter name and a one-line reason
- [ ] Security linters (`gosec`, `bodyclose`, `sqlclosecheck`) are enabled where applicable

## References

- [assets/.golangci.yml](assets/.golangci.yml) — production-ready baseline configuration
- [references/linter-catalog.md](references/linter-catalog.md) — what each linter checks and when to enable it
- [references/nolint-directives.md](references/nolint-directives.md) — suppression patterns, scoping rules, anti-patterns
- [references/ci-integration.md](references/ci-integration.md) — GitHub Actions, pre-commit hooks, incremental adoption

Referenced files: 5

go-logging8.48 KB

View saved version →

---
name: go-logging
description: "Use when choosing a Go logger, configuring slog, writing structured log statements, picking log levels, or attaching request-scoped fields. Apply proactively whenever code calls log/fmt to emit operational information, migrates off log/logrus/zap/zerolog, or sets up production logging. Covers structured logging only — metrics, traces, profiling, and RUM belong to a separate observability skill."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+ for log/slog. Go 1.26 slog.NewMultiHandler is noted where relevant."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Logging

Logs are written for **operators** — the human who will be paged at 3 a.m. and needs to know what happened. Every log line either helps diagnose a production issue or it is noise. `log/slog` from the standard library is the default; reach for anything else only after measuring.

> This skill covers **logging only**. Metrics, distributed tracing, profiling, and RUM are a separate concern — they belong to a future `go-observability` skill. Do not confuse them with logging here.

## Core Rules

1. **Use `log/slog`** for new code. Structured, leveled, in the standard library since Go 1.21.
2. **Static message, structured fields.** The message describes what happened; data goes in key-value attributes.
3. **Log or return, never both.** Logging a wrapped error makes the same failure appear at every layer.
4. **Log at the boundary.** HTTP handlers, job runners, and `main` log. Library code wraps and returns.
5. **Use snake_case keys** consistently across the codebase (`user_id`, `request_id`, `elapsed_ms`).
6. **`slog.Error` always carries an `"err"` attribute.** Without it, you logged a sentence, not an error.
7. **Never log secrets, PII, or unbounded data.** Tokens, full credit cards, request bodies — none of it.

## Choosing a Logger

| Situation | Use |
|---|---|
| New production service | `log/slog` |
| Trivial CLI / one-off script | `log` (the standard package) |
| Measured hot-path bottleneck where slog dominates the flame graph | `zap` or `zerolog`, but keep the structured style |
| Existing zap/logrus/zerolog code | Migrate to `slog` with a bridge handler; see [references/slog-handler-ecosystem.md](references/slog-handler-ecosystem.md) |

`slog`'s API is stable, the ecosystem has consolidated around it, and JSON output works with every log shipper. Do not introduce a third-party logger without a benchmark showing the win.

## Structured Logging

Build log messages from a **static message** plus typed fields:

```go
// Good — static message, structured fields
slog.Info("order placed", "order_id", orderID, "total_cents", totalCents)

// Bad — dynamic data baked into the message string
slog.Info(fmt.Sprintf("order %d placed for $%.2f", orderID, total))
```

The aggregator (Loki, Elastic, CloudWatch) can index `order_id`. It cannot index a sprintf'd sentence.

For hot paths, typed constructors avoid allocations:

```go
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
    slog.String("method", r.Method),
    slog.Int("status", code),
    slog.Duration("elapsed", elapsed),
)
```

## Log Levels

| Level | When | Default |
|---|---|---|
| `Debug` | Developer-only diagnostics; tracing internal state | Disabled in prod |
| `Info` | Notable lifecycle events: startup, shutdown, config loaded | Enabled |
| `Warn` | Unexpected but recoverable: retry succeeded, deprecated flag used | Enabled |
| `Error` | Operation failed; someone should look | Enabled |

Rules of thumb:

- If nobody should act on it, it is not `Error` — use `Warn` or `Info`.
- If it is only useful with a debugger attached, it is `Debug`.
- `slog.Error` must include an `"err"` attribute.

```go
slog.Error("payment failed", "err", err, "order_id", id)
slog.Warn("retry succeeded", "attempt", n, "endpoint", url)
slog.Info("server started", "addr", addr)
slog.Debug("cache lookup", "key", key, "hit", hit)
```

> Read [references/levels-and-context.md](references/levels-and-context.md) when choosing between `Warn` and `Error`, defining custom verbosity levels, or pre-checking `Enabled()` on hot paths.

## Request-Scoped Logging

Derive a logger per request that carries the fields every downstream call should include:

```go
func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log := slog.With("request_id", requestID(r))
        ctx := context.WithValue(r.Context(), loggerKey{}, log)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func FromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l }
    return slog.Default()
}
```

Use the `Context`-aware variants (`slog.InfoContext`, `slog.ErrorContext`) so handlers that read trace IDs from the context can stamp them into the record:

```go
slog.InfoContext(ctx, "order placed", "order_id", id)
```

> Read [references/request-scope-and-middleware.md](references/request-scope-and-middleware.md) when wiring request IDs, building logging middleware, or choosing between context-stored loggers and explicit parameters.

## Log or Return — Not Both

Logging an error and then returning it produces the same failure at every layer, and three log records for one bug:

```go
// Bad — every caller up the stack logs it again
if err != nil {
    slog.Error("query failed", "err", err)
    return fmt.Errorf("query: %w", err)
}

// Good — wrap and return; the boundary logs once
if err != nil {
    return fmt.Errorf("loading user %d: %w", id, err)
}
```

The **only** layer that logs is the one that finishes the work: the HTTP handler, the job runner, `main`. That layer may log a detailed record server-side while returning a sanitised message to the client:

```go
if err := checkout(ctx); err != nil {
    slog.ErrorContext(ctx, "checkout failed", "err", err, "user_id", uid)
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}
```

See the `go-error-handling` skill for the full handle-once pattern.

## What Not to Log

- Passwords, API keys, tokens, session IDs.
- Full credit card numbers, SSNs, government IDs.
- Request or response bodies that may contain user data.
- Whole slices or maps of unbounded size (log lengths instead).
- Anything you would not want appearing in a customer support screenshot.

Use a redacting `slog.Handler` (or wrap your own) so sensitive keys are blanked at the handler level, not at every call site. See [references/slog-handler-ecosystem.md](references/slog-handler-ecosystem.md).

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `log.Printf("msg %v", v)` | Unstructured; impossible to index | `slog.Info("msg", "key", v)` |
| `fmt.Sprintf` inside the message | Data is now part of the string | Static message + key/value attrs |
| Logging and returning the same error | Duplicate log records, noisy alerts | Wrap and return; log at the boundary |
| `slog.Info("err: %v", err)` | Drops level semantics and structure | `slog.Error("op failed", "err", err)` |
| New logger per call | Loses request-scoped fields | Derive once in middleware, pass via context |
| Mixed key styles (`userId`, `user_id`, `UserID`) | Aggregators index them as different fields | Pick `snake_case` and stick to it |
| Logging the whole request body | Leaks PII; explodes log volume | Log lengths and content type only |
| Introducing zap/zerolog without a benchmark | Extra dependency for no measured win | Stay on `slog`; benchmark before switching |

## Verification Checklist

Before finishing a logging change:

- [ ] All new log calls use `log/slog`, not `log.Printf`
- [ ] Each call has a static message and key-value attributes
- [ ] `slog.Error` calls carry an `"err"` attribute
- [ ] No call both logs and returns the same error
- [ ] Keys use `snake_case` and match existing keys in the codebase
- [ ] Handlers use `*Context` variants so trace correlation works
- [ ] No secrets, PII, or unbounded values appear in attribute values
- [ ] Request-scoped fields are added in middleware, not at each call site

## References

- [references/levels-and-context.md](references/levels-and-context.md) — picking levels, `Enabled()` gating, custom verbosity
- [references/request-scope-and-middleware.md](references/request-scope-and-middleware.md) — request IDs, context-stored loggers, HTTP middleware
- [references/slog-handler-ecosystem.md](references/slog-handler-ecosystem.md) — JSON/text handlers, multi-handler, bridges from zap/logrus/zerolog, redaction

Referenced files: 4

go-naming8.06 KB

View saved version →

---
name: go-naming
description: "Use when naming any Go identifier — packages, types, functions, methods, receivers, variables, constants, errors, options. Covers MixedCaps, scope-based length, initialism casing, the no-`Get` rule, `-er` interfaces, sentinel `ErrX` vs typed `XError`, and the most commonly missed conventions (constructors, boolean fields, enum zero values, lowercase error strings). Apply proactively whenever new identifiers are introduced, even if the user has not asked about naming."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Go 1.0+ for the core rules; iota/enum guidance is version-neutral."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Naming Conventions

Go uses naming to encode visibility (`UpperCamelCase` = exported, `lowerCamelCase` = unexported), so naming is load-bearing — not cosmetic. Names should be **short, contextual, and non-repetitive**. The package name is always present at the call site; pretending otherwise is the single biggest source of bad Go names.

## Core Rules

1. **MixedCaps only.** No underscores, no `SCREAMING_SNAKE_CASE`, no `kHungarian`. Exceptions: test subtests (`TestFoo_BadInput`), generated code, cgo.
2. **Capitalization is visibility.** `Exported`, `unexported`. Do not invent other conventions.
3. **No stuttering.** The package name is at the call site; `http.HTTPClient` is wrong, `http.Client` is right.
4. **Scope drives length.** `i` is fine in a 3-line loop; package-level vars need descriptive names.
5. **Initialisms keep one case.** `userID`, `HTTPServer`, `ParseURL` — never `userId`, `HttpServer`, `ParseUrl`.
6. **Receivers are 1-2 letter abbreviations**, consistent across all methods of the type. Never `this`/`self`.

## Naming Decision Flow

```
What are you naming?
├─ Package        → lowercase single word, singular, specific (not util/common/helper)
├─ File           → lowercase, underscores OK (user_handler.go)
├─ Interface      → method + "-er" when single-method (Reader, Closer, Stringer)
├─ Struct/Type    → MixedCaps noun (Request, FileHeader)
├─ Constructor    → New() if package has one primary type; NewThing() if multiple
├─ Constant       → MixedCaps; never ALL_CAPS; role-based not value-based
├─ Enum (iota)    → type-prefix + Unknown/Invalid at position 0
├─ Sentinel error → ErrXxx (var ErrNotFound = errors.New("..."))
├─ Error type     → XxxError (type PathError struct{})
├─ Boolean field  → is/has/can prefix (isReady, hasPerm)
├─ Getter         → field name only (Owner()), never GetOwner()
├─ Setter         → SetXxx (SetOwner)
├─ Option         → WithXxx (WithLogger, WithPort)
├─ Variant        → WithContext suffix, In suffix (in-place), Must prefix (panics)
└─ Variable       → length proportional to scope distance
```

## Quick Reference Table

| Element | Convention | Example |
|---|---|---|
| Package | lowercase, singular | `http`, `tabwriter` |
| Exported | `UpperCamelCase` | `ReadAll`, `HTTPClient` |
| Unexported | `lowerCamelCase` | `parseToken`, `userCount` |
| Receiver | 1-2 letters | `func (s *Server)` |
| Constant | MixedCaps | `MaxRetries`, `defaultTimeout` |
| Initialism | uniform case | `URL`, `HTTPServer`, `xmlParser` |
| Sentinel error | `Err` prefix | `ErrNotFound` |
| Error type | `Error` suffix | `*PathError` |
| Boolean field | `is`/`has`/`can` | `isConnected` |
| Option func | `With` + field | `WithPort(8080)` |
| Format func | `f` suffix | `Errorf`, `Wrapf` |

## Frequently Missed Conventions

These are correct but non-obvious — they account for most naming mistakes in code review.

### Constructor: `New` vs `NewThing`

If the package exports **one primary type**, the constructor is `New()`. Callers write `apiclient.New()`, not `apiclient.NewClient()`. Only use `NewThing` when the package builds several things (`http.NewRequest`, `http.NewServeMux`).

### Boolean Fields Get a Prefix

Unexported boolean fields use `is`/`has`/`can`. A bare adjective is ambiguous — is `connected` a method or a field, a state or a verb past tense?

```go
type Conn struct { isOpen bool }
func (c *Conn) IsOpen() bool { return c.isOpen }
```

### Error Strings Are Fully Lowercase

Including acronyms. Errors get concatenated: `fmt.Errorf("parsing token: %w", err)` becomes `"parsing token: invalid message id"`. Mid-sentence capitals look wrong. Use `"invalid message id"` not `"invalid message ID"`.

Sentinel errors should include the package name: `errors.New("apiclient: not found")`.

### Enum Zero Value Is a Sentinel

`var s Status` is silently `0`. If `0` is `StatusReady`, uninitialised values look intentional. Put `StatusUnknown` (or `Invalid`) at iota 0.

```go
type Status int
const (
    StatusUnknown Status = iota // zero-value catch
    StatusReady
    StatusRunning
)
```

### Subtest Names Are Lowercase Phrases

```go
t.Run("valid id", ...) // not "Valid ID"
t.Run("empty input", ...)
```

> Read [references/types-errors-constants.md](references/types-errors-constants.md) when naming new struct/interface/enum/error families.

## MixedCaps Is Load-Bearing

```go
MaxPacketSize    // good
userCount        // good
parseHTTPResponse // good

MAX_PACKET_SIZE  // wrong — Go reserves casing for visibility
max_packet_size  // wrong — snake_case
kMaxBufferSize   // wrong — Hungarian
```

## Avoid Stuttering

The package name is always present at the call site.

```go
// In package http
type Client struct{} // not HTTPClient — caller writes http.Client

// In package user
func New() *User // not NewUser — caller writes user.New()

// In package dbpool
type Pool struct{}    // not DBPool
type Option func()    // not PoolOption
```

> Read [references/identifiers-and-scope.md](references/identifiers-and-scope.md) for receivers, variable scope rules, and import aliasing.

## Avoid Built-In Names

Never shadow `error`, `string`, `len`, `cap`, `append`, `copy`, `new`, `make`, `nil`, `iota`. The compiler allows it; readers and tools do not.

## Anti-Patterns

| Mistake | Fix |
|---|---|
| `MAX_RETRIES = 3` constant | `MaxRetries = 3` — MixedCaps |
| `GetName() string` getter | `Name() string` — Go omits `Get` |
| `HttpClient`, `UserId`, `ParseUrl` | `HTTPClient`, `UserID`, `ParseURL` — uniform initialism case |
| `this`/`self` receiver | One-letter abbreviation (`s` for `Server`) |
| `util`, `common`, `helpers` package | Specific name that describes content (`stringutil`, `httpauth`) |
| `user.NewUser()` constructor | `user.New()` — drop the type name |
| `connected bool` field | `isConnected bool` — prefix reads as a question |
| `"invalid message ID"` error | `"invalid message id"` — fully lowercase |
| `StatusReady` at iota 0 | Add `StatusUnknown` at 0 |
| `userSlice []User` | `users []User` — types do not belong in names |

## Verification Checklist

- [ ] No identifier contains `_` outside of test subtests, generated code, or cgo.
- [ ] No `Get` prefix on getters; setters use `Set`.
- [ ] Initialisms are uniform case (grep `Url\|Http\|Json\|Xml\|Id\b` in source).
- [ ] Receivers across one type all use the same short name.
- [ ] All sentinel errors are `ErrXxx`; all error types are `*XxxError`.
- [ ] All iota-based enums place a `Unknown`/`Invalid` value at position 0.
- [ ] No package named `util`, `common`, `helpers`, `misc`.

## Enforce With Linters

Most rules are mechanical and a linter will catch them in CI:

- `revive` — `var-naming`, `exported`, `receiver-naming`, `error-naming`.
- `predeclared` — flags identifiers that shadow built-ins.
- `errname` — enforces `ErrXxx` / `*XxxError`.
- `misspell` — keeps comments and identifiers consistent.

Add them to `.golangci.yml` and run `golangci-lint run` in CI.

## References

- [references/identifiers-and-scope.md](references/identifiers-and-scope.md) — receivers, scope-based length, acronyms, import aliasing
- [references/types-errors-constants.md](references/types-errors-constants.md) — interfaces, structs, enums, sentinel vs typed errors
- [references/functions-and-options.md](references/functions-and-options.md) — constructors, getters, variants, functional options

Referenced files: 4

go-observability8.26 KB

View saved version →

---
name: go-observability
description: "Use when instrumenting Go services with metrics and distributed traces, or wiring exemplars and request-id propagation. Covers Prometheus patterns (Counter/Gauge/Histogram, low-cardinality labels), OpenTelemetry tracing (TracerProvider, span attributes, errors, context propagation), and metric ↔ trace correlation so a P99 spike jumps to the offending trace. Logging: see go-logging."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+ (for log/slog context variants used in correlation snippets). Prometheus client_golang and OpenTelemetry Go SDK."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Observability — Metrics, Traces, and Correlation

Production Go services need at least two always-on signals to be debuggable: **metrics** (aggregated measurements for alerting and SLOs) and **traces** (per-request flow showing where time went). The third deliverable is **correlation**: a P99 metric spike must lead, in one click, to the trace that caused it.

> **Logging is not in this skill.** Structured logging with `log/slog`, log levels, and zap/logrus/zerolog migration belong to the **go-logging** skill. This skill only references logs in the context of correlating them with traces.

## Core Rules

1. **A feature is not done until it is observable.** New code MUST export at least: one Counter for operations, one Counter for errors, one Histogram for latency.
2. **Histograms, not Summaries, for latency.** Summaries cannot be aggregated across instances; Histograms support `histogram_quantile()` server-side.
3. **Label cardinality is bounded.** Never put unbounded values (user IDs, full URLs, request IDs) in Prometheus labels. Use route patterns, status classes, method.
4. **Context flows everywhere.** A function that does I/O takes `ctx context.Context` as its first argument. No context = no trace propagation = no correlation.
5. **Record errors on the span.** When a span ends in failure, call `span.RecordError(err)` and `span.SetStatus(codes.Error, ...)`. A green span hides a real failure.
6. **Correlate or it didn't happen.** Inject `trace_id` into logs, attach exemplars to histograms. Otherwise the three signals are three different products.

## Signal Decision

Pick the signal that matches the question. Do not log what should be a metric.

| Question | Signal | Tool |
|---|---|---|
| How often does X happen? Error rate? Rate-per-second? | Metric (Counter) | Prometheus |
| What is the P99 latency of endpoint /orders? | Metric (Histogram) | Prometheus + `histogram_quantile` |
| Where did this one slow request spend its time? | Trace | OpenTelemetry |
| Why does latency spike at 14:32? | Metric → exemplar → trace | Prometheus + OTel + exemplars |
| What concrete error message did this request hit? | Log (see go-logging) | `log/slog` |

> Read [references/metrics.md](references/metrics.md) for Counter/Gauge/Histogram patterns, naming, and PromQL-as-comments.
> Read [references/tracing.md](references/tracing.md) for TracerProvider setup, span attributes, and `otelhttp` middleware.

## Metrics — the 60-Second Setup

```go
import "github.com/prometheus/client_golang/prometheus"

// rate(http_requests_total{code=~"5.."}[5m]) / rate(http_requests_total[5m])
var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests by method, route, status class.",
    },
    []string{"method", "route", "code"}, // ALL bounded
)

// histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
var httpLatency = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request latency in seconds.",
        Buckets: prometheus.DefBuckets,
    },
    []string{"method", "route"},
)
```

The comment above each metric is the PromQL it is designed to answer. This makes the metric discoverable and grep-able from a dashboard.

## Traces — the 60-Second Setup

```go
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/codes"
)

func (s *OrderService) Create(ctx context.Context, in CreateOrderInput) (*Order, error) {
    ctx, span := otel.Tracer("order-service").Start(ctx, "OrderService.Create")
    defer span.End()
    span.SetAttributes(attribute.String("order.user_id", in.UserID))

    order, err := s.repo.Insert(ctx, in)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "insert failed")
        return nil, fmt.Errorf("creating order: %w", err)
    }
    return order, nil
}
```

Every service method, every DB query, every external HTTP call gets a span. Context **must** flow into `s.repo.Insert(ctx, ...)` so the DB span is a child of the service span.

## Correlation

### Metrics → Traces with Exemplars

An exemplar attaches a single trace_id to a histogram observation. In Grafana, click the dot on a P99 spike and you land on the trace.

```go
obs := httpLatency.WithLabelValues(r.Method, routePattern)
sc := trace.SpanContextFromContext(ctx)
if eo, ok := obs.(prometheus.ExemplarObserver); ok && sc.IsValid() {
    eo.ObserveWithExemplar(elapsed.Seconds(),
        prometheus.Labels{"trace_id": sc.TraceID().String()})
} else {
    obs.Observe(elapsed.Seconds())
}
```

### Logs → Traces

Use the `otelslog` bridge (see go-logging skill) so every `slog.InfoContext(ctx, ...)` call automatically emits `trace_id` and `span_id`. You can then grep logs by trace_id when starting from a trace, or jump from a log line to the trace.

> Read [references/correlation.md](references/correlation.md) for exemplar wiring details, request-id propagation, and the end-to-end "metric spike → trace → log" workflow.

## Context Propagation

```go
// Bad — breaks trace propagation; the DB call starts a new root trace.
result, err := db.Query("SELECT ...")

// Good — the DB span is a child of the HTTP request span.
result, err := db.QueryContext(ctx, "SELECT ...")
```

Every I/O boundary takes ctx: HTTP client, database, gRPC client, message queue. Use `otelhttp.NewTransport` to instrument outbound HTTP and `otelhttp.NewHandler` for inbound.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `Summary` for latency | Cannot aggregate across replicas; no quantile flexibility | `Histogram` + `histogram_quantile()` |
| `userID` as a Prometheus label | Unbounded cardinality → Prometheus OOM | Hash to a bucketed `user_tier` or drop |
| Logging the same error you return | Duplicate lines, no single source of truth | Return wrapped, log once at the boundary |
| No `RecordError` on failed spans | Trace shows green, alert fires red | Always pair error returns with `span.RecordError` + `SetStatus` |
| Trace without context propagation | Each layer starts a new root trace | First argument of every I/O method is `ctx context.Context` |
| Global metric defined inside a handler | Re-registers on every call → panic | Declare metrics at package level, register once |
| Histogram with 200 buckets | High storage cost, slow queries | Start with `DefBuckets`, tune from real data |

## Verification Checklist

Before marking the change done:

- [ ] Each new code path emits at least one counter (operations) and one histogram (latency)
- [ ] All metric labels are bounded (method, route pattern, status class — not IDs)
- [ ] Each PromQL query the metric is meant to answer appears as a comment above the metric declaration
- [ ] Every I/O method takes `ctx` first; downstream calls pass it through
- [ ] Every service method, DB call, and outbound HTTP call has a span
- [ ] Failure paths call `span.RecordError(err)` and `span.SetStatus(codes.Error, ...)`
- [ ] At least one histogram uses `ObserveWithExemplar` for trace correlation
- [ ] Logs and traces are correlated (see go-logging skill: `otelslog` bridge)

## References

- [references/metrics.md](references/metrics.md) — Counter/Gauge/Histogram, naming, cardinality, PromQL examples
- [references/tracing.md](references/tracing.md) — TracerProvider, spans, attributes, `otelhttp`, sampling
- [references/correlation.md](references/correlation.md) — Exemplars, trace_id in logs, end-to-end workflow
- [references/anti-patterns.md](references/anti-patterns.md) — Detailed walkthrough of each anti-pattern with code

Referenced files: 5

go-packages6.27 KB

View saved version →

---
name: go-packages
description: "Use when creating Go packages, organizing imports, managing dependencies, or structuring a Go project. Covers meaningful package names, package size, import grouping (stdlib first, then external), blank/dot imports, the run() pattern in main, init() restrictions, and CLI flag conventions. Apply proactively when starting a new module or splitting a growing codebase, even if the user did not explicitly ask about package layout. Does not cover identifier naming inside packages (see go-naming)."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Plain Go (any supported version)."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Packages and Imports

A package is a unit of meaning, not a folder of files. Name it for what it provides, keep imports tidy, and put startup logic where it belongs.

## Core Rules

1. **Package names describe what the package provides.** `util`, `helper`, `common`, `misc` are not names.
2. **Imports are grouped: stdlib first, then external.** `goimports` will keep this honest.
3. **Avoid `init()`** — and when unavoidable, keep it deterministic and I/O-free.
4. **`os.Exit` / `log.Fatal` only inside `main`.** Library code returns errors.
5. **Use the `run()` pattern** so `main` has a single exit point and deferred cleanup runs.
6. **CLI flags belong in `package main`.** Libraries take configuration as parameters.
7. **Blank imports** belong in `main` or tests. **Dot imports** are essentially never appropriate.

## Decision: How to Split a Package

| Question | If "yes" |
|---|---|
| Can you state the package's purpose in one sentence? | Probably right-sized |
| Do its files never share unexported symbols? | Likely two packages glued by directory |
| Do distinct caller groups touch distinct files? | Split along caller boundaries |
| Is the godoc index so long callers cannot find things? | Split for discoverability |
| Does splitting create import cycles? | Don't split |

> Read [references/package-layout.md](references/package-layout.md) when deciding how to split a growing package, organizing `cmd/`, `internal/`, or designing a library API surface.

## Naming Packages

```go
// Good — meaningful
db := spannertest.NewDatabaseFromFile(...)
_, err := f.Seek(0, io.SeekStart)

// Bad — vague
db := test.NewDatabaseFromFile(...)
_, err := f.Seek(0, common.SeekStart)
```

Generic words may appear as part of a name (`stringutil`, `iotest`) but not as the whole name. Match the package to a concept the caller already knows.

## Imports

```go
import (
    "fmt"
    "os"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)
```

| Rule | Guidance |
|---|---|
| Group order | stdlib, then external; extended order may also separate protos and side-effect imports |
| Renaming | Avoid unless there is a collision; rename the more-local import |
| Blank import (`import _`) | Only `main` and tests |
| Dot import (`import .`) | Effectively never; rare in test files for circular deps |

> Read [references/imports-and-main.md](references/imports-and-main.md) for extended import grouping, proto `pb` suffixes, the `run()` pattern, and CLI flag conventions.

## Avoid `init()`

When you must use `init()`, make it:

1. Deterministic — same result every run.
2. Independent of the order of other `init()`s.
3. Free of environment state (env vars, working dir, args).
4. Free of I/O (filesystem, network, syscalls).

Acceptable uses:

- Precomputing a constant that cannot fit in a single expression.
- Registering pluggable hooks (`database/sql` drivers).

If your `init` reads a file or calls a network API, refactor it into an explicit `Setup()` the caller invokes.

## Exit Only in `main`

```go
func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    // all the real work
    return nil
}
```

Why:

- `log.Fatal` and `os.Exit` skip `defer`. Anywhere except `main`, that means leaked files, half-flushed buffers, undeleted temp dirs.
- The `run()` pattern gives you one place to log a clean error and one place to set the exit code.

## CLI Flags

- Define flags in `package main`.
- Flag names use `snake_case`: `--output_dir`, not `--outputDir`.
- Libraries accept configuration through function parameters, never reach for `flag.Lookup`.

```go
func main() {
    outputDir := flag.String("output_dir", ".", "directory for output files")
    flag.Parse()
    if err := mylib.Generate(*outputDir); err != nil {
        log.Fatal(err)
    }
}
```

> Read [references/init-and-globals.md](references/init-and-globals.md) for the boundaries between safe init-time computation, mutable globals, and dependency injection.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| `package util` | Meaningless name; import conflicts | Name after the concept |
| One huge package with 50 files | Hard to navigate, slow builds | Split by responsibility |
| `init()` reads config from disk | Side effect at import time | Explicit `Setup()` in `main` |
| `log.Fatal` in library code | Skips defers, untestable | Return an error |
| `os.Exit` in a request handler | Same — plus crashes the server | Return an error to the framework |
| `import _ "pkg"` in a library | Side effects on every importer | Register explicitly |
| `import . "pkg"` to "save typing" | Tools lose track of where names come from | Use the package qualifier |
| Library reads a flag at import time | Untestable, non-reusable | Accept config as parameter |

## Verification Checklist

- [ ] Package name is concrete and unambiguous
- [ ] Imports are grouped (stdlib first), `goimports` clean
- [ ] No `init()` performs I/O or depends on env state
- [ ] `main` is a single `if err := run(); err != nil { log.Fatal(err) }`
- [ ] No `os.Exit` / `log.Fatal*` outside `main`
- [ ] Flags are defined only in `package main`
- [ ] No `import .` and no blank import outside `main`/tests
- [ ] Package's purpose fits in one sentence

## References

- [references/package-layout.md](references/package-layout.md) — splitting packages, `cmd/`, `internal/`, public API surface
- [references/imports-and-main.md](references/imports-and-main.md) — extended import grouping, the `run()` pattern, flag conventions
- [references/init-and-globals.md](references/init-and-globals.md) — when `init` is acceptable, mutable globals, DI

Referenced files: 4

go-performance7.59 KB

View saved version →

---
name: go-performance
description: "Use when profiling, benchmarking, or optimizing Go code — includes the measure-first methodology, the pprof-driven decision tree (which symptom maps to which fix), allocation reduction, capacity hints, hot-path patterns (strconv vs fmt, repeated string→byte conversions, strings.Builder), and runtime tuning. Apply proactively whenever a user mentions slowness, allocations, GC pressure, or asks for benchmarks, even if no specific pattern is named."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Methodology is Go-version-neutral; `b.Loop()` and PGO require Go 1.21+/1.24+."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Performance

Performance work in Go follows one rule: **measure first**. Intuition about bottlenecks is wrong roughly 80% of the time. Profile, hypothesise, change *one thing*, re-measure. The patterns in this skill apply only on hot paths — premature optimisation makes code worse without making it faster.

## Core Rules

1. **Profile before optimising.** `go test -bench`, `pprof`, `fgprof` — never guess.
2. **One change at a time.** Multi-change "optimisation" passes are unreviewable.
3. **Compare with `benchstat`.** Single runs lie; you need ≥6 runs to see signal.
4. **Allocation reduction usually beats CPU micro-optimisation** — the GC is fast but not free.
5. **Rule out external bottlenecks first.** If 90% of latency is the DB, faster Go code is irrelevant.
6. **Document optimisations in comments.** Future readers will revert "ugly" code without context.

## Iterative Methodology

The cycle is: **define goal → write benchmark → measure baseline → diagnose → improve one thing → re-measure → commit with the diff.**

```bash
# baseline
go test -bench=BenchmarkHotPath -benchmem -count=6 ./pkg/... | tee /tmp/report-1.txt

# (apply ONE change)

# compare
go test -bench=BenchmarkHotPath -benchmem -count=6 ./pkg/... | tee /tmp/report-2.txt
benchstat /tmp/report-1.txt /tmp/report-2.txt
```

If `benchstat` shows no statistically significant change, the optimisation didn't work — revert it. Keep the `/tmp/report-*.txt` files as an audit trail; paste the `benchstat` output in the commit body.

> Read [references/benchmarking-and-pprof.md](references/benchmarking-and-pprof.md) for benchmark writing, pprof workflow, and `b.Loop()` (Go 1.24+).

## Rule Out External Bottlenecks First

Before optimising any Go code, check that the bottleneck is actually in your process:

- **`fgprof`** — captures on-CPU and off-CPU (I/O wait) time. If off-CPU dominates, the issue is elsewhere.
- **Goroutine profile** — many goroutines blocked in `net.(*conn).Read` or `database/sql` means external I/O is the limit.
- **Distributed tracing** — span breakdown shows which upstream is slow.

If the bottleneck is external (DB, downstream API, disk), fix that — query tuning, indexes, connection pools, caching. No Go-level change will help.

## Decision Tree: Where Is Time Spent?

| Symptom (from pprof) | Action |
|---|---|
| High `alloc_objects` / `alloc_space` | reduce allocations (preallocate, pool, struct fields) |
| One function dominates CPU profile | inline-friendly rewrite, avoid reflection, simpler algorithm |
| High GC% / OOM kills | tune `GOMEMLIMIT`, `GOGC`; reduce live heap |
| Goroutines blocked on I/O | concurrency, batching, connection pool tuning |
| Same computation many times | memoise / `singleflight` / cache |
| Wrong algorithm (O(n²) where O(n) exists) | fix algorithm before anything else |
| Mutex profile hot | reduce critical section, sharded locks, `sync.Pool` |

> Read [references/allocation-and-memory.md](references/allocation-and-memory.md) for allocation patterns, `sync.Pool`, struct alignment, and escape analysis.

## Concrete High-ROI Patterns

These are the small changes that consistently show up in profiles. Apply them when the symptom matches — not preemptively.

### 1. `strconv` over `fmt` for primitives

```go
// Bad — fmt parses a format string
s := fmt.Sprint(n)

// Good — direct conversion, ~2x faster, half the allocations
s := strconv.Itoa(n)
```

| | ns/op | allocs |
|---|---|---|
| `fmt.Sprint(n)` | ~143 | 2 |
| `strconv.Itoa(n)` | ~64 | 1 |

### 2. Move constant `[]byte` conversions out of loops

```go
// Bad — allocates on every iteration
for i := 0; i < n; i++ {
    w.Write([]byte("hello"))
}

// Good — convert once
hello := []byte("hello")
for i := 0; i < n; i++ {
    w.Write(hello)
}
```

About 7x faster in a tight loop.

### 3. Preallocate slice and map capacity

```go
// Bad — repeated growth, O(n) copies per growth
out := []Result{}
for _, x := range input {
    out = append(out, transform(x))
}

// Good — zero reallocations
out := make([]Result, 0, len(input))
for _, x := range input {
    out = append(out, transform(x))
}
```

Slice capacity is **exact**: `make([]T, 0, n)` allocates exactly `n` slots. Map capacity is a **hint** about bucket count, but still avoids the worst rehashes.

| | Time |
|---|---|
| no capacity | ~2.48s |
| with capacity | ~0.21s |

About 12x faster on the synthetic benchmark.

### 4. `strings.Builder` for loop-built strings

`s += w` in a loop is O(n²). Use `strings.Builder`, with `Grow(n)` when the final size is estimable.

### 5. Pass small fixed-size values

`*string`, `*int`, `*time.Time` add indirection without saving anything — strings and time.Time are already small headers. Use pointers only for mutation, types ~128B+, types embedding sync primitives, or where `nil` is meaningful.

> Read [references/concrete-patterns.md](references/concrete-patterns.md) for the full pattern catalogue with benchmark numbers.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Optimising without `pprof` | wrong target, wasted effort | profile first |
| Default `http.Client` for high-throughput callers | `MaxIdleConnsPerHost: 2` bottleneck | configure `Transport` |
| Logging inside hot loops | prevents inlining, allocates even when disabled | `slog.LogAttrs`, gate by level |
| `panic`/`recover` as control flow | stack trace allocation | error returns |
| `reflect.DeepEqual` in production | 50-200x slower than typed comparison | `slices.Equal`, `maps.Equal`, `bytes.Equal` |
| `unsafe` without a benchmark | rarely justified | benchmark + comment with numbers |
| No `GOMEMLIMIT` in containers | OOM kills under load | set to ~80% of container limit |

## Verification Checklist

- [ ] A benchmark exists for the function being optimised.
- [ ] Baseline `/tmp/report-1.txt` was captured before any change.
- [ ] Each change is a single commit with `benchstat` output in the body.
- [ ] `benchstat` shows the change is statistically significant (`p < 0.05`).
- [ ] Profile (`pprof`) confirms the targeted hotspot actually moved.
- [ ] Optimisations on production paths have an explanatory comment.
- [ ] `GOMEMLIMIT` is configured for any containerised long-running process.

## Enforce With Linters

Mechanical anti-patterns belong to CI:

- `gocritic` — flags `fmt.Sprint(x)` for primitives, repeated allocations.
- `prealloc` — slices that could be preallocated.
- `gocyclo` / `funlen` — proxies for code that is hard to optimise.
- `fieldalignment` (go vet) — struct layout for memory reduction.

## References

- [references/benchmarking-and-pprof.md](references/benchmarking-and-pprof.md) — writing benchmarks, `benchstat`, pprof workflow, `b.Loop()`
- [references/allocation-and-memory.md](references/allocation-and-memory.md) — escape analysis, `sync.Pool`, struct alignment, backing-array leaks
- [references/concrete-patterns.md](references/concrete-patterns.md) — full pattern catalogue with numbers

Referenced files: 4

go-swagger7.79 KB

View saved version →

---
name: go-swagger
description: "Use when adding or maintaining OpenAPI/Swagger documentation for a Go HTTP API. Covers swaggo/swag annotation comments (@Summary, @Param, @Success, @Router, @Security), the swag CLI workflow, framework integration for Gin/Echo/Fiber/Chi/net-http, security definitions (Bearer/JWT, OAuth2, API key), and struct tags (example, enums, swaggertype, swaggerignore). Apply when a project imports github.com/swaggo/swag or any of the swaggo UI adapters, or when you need to expose /swagger/index.html."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Requires Go 1.21+, swaggo/swag v1.16+ (CLI: `swag`)."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Swagger / OpenAPI with swaggo

`github.com/swaggo/swag` is the de-facto annotation-driven OpenAPI generator for Go. You annotate handlers with `// @...` comments, run the `swag` CLI, and get `docs/swagger.json`, `docs/swagger.yaml`, and `docs/docs.go` for the UI.

## Core Rules

1. **Docs are a contract.** A field documented as required is the API's promise; a mismatch with the implementation is a bug.
2. **Annotations live next to handlers.** Not in a separate `docs/` folder — comments rot when separated from code.
3. **Regenerate on every change.** `swag init` is part of the build (`go generate` or a Makefile target). Stale `docs/` is worse than no docs.
4. **The `docs` package must be imported.** A blank import (`_ "yourmod/docs"`) registers the spec at process start.
5. **Use named structs for request/response bodies.** swag cannot derive a schema from `map[string]any` or a primitive type.
6. **Security definitions match implementation.** If the API enforces JWT, declare `@securityDefinitions.apikey Bearer` and annotate every protected endpoint with `@Security Bearer`.

## Install and Bootstrap

```bash
go install github.com/swaggo/swag/cmd/swag@latest
swag init                              # general info from main.go
swag init -g cmd/api/main.go           # custom main path
swag fmt                               # format annotation comments like gofmt
```

Wire the UI for your framework — choose one:

```go
// Gin
import (
    swaggerFiles "github.com/swaggo/files"
    ginSwagger  "github.com/swaggo/gin-swagger"
)
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

// Echo
r.GET("/swagger/*", echoSwagger.WrapHandler)

// Fiber
app.Get("/swagger/*", fiberSwagger.WrapHandler(swaggerFiles.Handler))

// Chi / net/http
mux.Handle("/swagger/", httpSwagger.Handler(swaggerFiles.Handler))
```

Import the generated spec:

```go
import _ "github.com/acme/myapi/docs"          // blank: just register
import docs "github.com/acme/myapi/docs"       // named: override host at runtime
```

> Read [references/swag-cli.md](references/swag-cli.md) for the CLI flag inventory and Makefile patterns.

## General API Info

Place in the file passed via `-g` (usually `main.go`):

```go
// @title           Orders API
// @version         1.0
// @description     Orders, customers, shipments.
// @contact.name    API Support
// @contact.email   api@acme.example
// @license.name    Apache-2.0
// @host            api.acme.example
// @BasePath        /api/v1
// @schemes         https http

// @securityDefinitions.apikey Bearer
// @in   header
// @name Authorization
// @description Use "Bearer <token>"
```

For multi-environment deployments, set host/basepath at runtime instead of hard-coding:

```go
import docs "github.com/acme/myapi/docs"

func main() {
    docs.SwaggerInfo.Host     = os.Getenv("API_HOST")
    docs.SwaggerInfo.BasePath = "/api/v1"
    // ...
}
```

## Operation Annotations

```go
// GetOrder godoc
// @Summary      Get an order by ID
// @Tags         orders
// @Produce      json
// @Param        id   path  string  true  "Order ID (UUID)"
// @Success      200  {object}  api.OrderResponse
// @Failure      404  {object}  api.ErrorResponse
// @Router       /orders/{id} [get]
// @Security     Bearer
func GetOrder(c *gin.Context) { /* ... */ }
```

**`@Param`:** `@Param <name> <in> <type> <required> "<desc>" [attributes]` — `<in>` is one of `path`, `query`, `body`, `header`, `formData`. Useful attributes: `default(v)`, `minimum(n)`, `maximum(n)`, `Enums(a,b,c)`, `example(v)`, `collectionFormat(multi)`.

**`@Success` / `@Failure`:** `@<kw> <code> {<kind>} <type> "<desc>"` — `{object}` (struct), `{array}` (slice), or a primitive (`string`, `integer`). Generics (swag v2): `api.Response[model.Order]`. Composition: `api.Response{data=model.Order}`.

> Read [references/annotations.md](references/annotations.md) for the full annotation grammar, edge cases, and security definitions.

## Security

Declare schemes once globally (`@securityDefinitions.apikey Bearer`, `@securityDefinitions.oauth2.authorizationCode`, `@securityDefinitions.basic`) and apply per endpoint:

```go
// @Security Bearer
// @Security OAuth2[read, write]
// @Security BasicAuth && Bearer   // both required (AND)
```

Endpoints without `@Security` are documented as public — match the implementation.

## Struct Tags

Enrich models without changing their Go type. Common tags: `example`, `enums:"a,b,c"`, `minimum`/`maximum`, `minLength`/`maxLength`, `format`, `swaggertype` (override detected type, e.g. `time.Time` → string), `swaggerignore:"true"`, and `extensions:"x-nullable,x-deprecated=true"`.

```go
type CreateOrderRequest struct {
    Status   string    `json:"status" enums:"pending,paid,shipped"`
    Total    int64     `json:"total" minimum:"0" example:"19999"`
    PlacedAt time.Time `json:"placed_at" swaggertype:"string" format:"date-time"`
    Internal string    `json:"-" swaggerignore:"true"`
}
```

> Read [references/struct-tags.md](references/struct-tags.md) for type overrides (`time.Time`, `uuid.UUID`, `decimal.Decimal`, custom scalars) and NULL handling.

## Make Target

```makefile
.PHONY: docs
docs:
	swag fmt
	swag init -g cmd/api/main.go --parseDependency --parseInternal

check-docs: docs
	@git diff --quiet docs || (echo "docs/ is stale; run make docs"; exit 1)
```

Run `make check-docs` in CI to catch annotation drift before merge.

## Anti-Patterns

| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Forgetting `_ "yourmod/docs"` | UI loads empty, no errors | Add the blank import in main |
| `@Param body string` | swag cannot derive a schema from a primitive | Use a named struct |
| Stale `docs/` after handler change | Docs lie to clients | Regenerate in CI; fail on drift |
| General info in wrong file | Spec has no title/host | Use `-g <file>` or move to main |
| `{object} map[string]any` | swag silently empty | Define a wrapper struct |
| No `@Security` on protected route | UI shows no lock icon | Add `@Security` everywhere auth is required |
| Multi-word `@Tags` unquoted | Tags split on whitespace | Quote: `@Tags "order management"` |
| Exposing `/swagger/*` in production unconditionally | Public API surface map | Gate behind env flag or auth |

## Verification Checklist

- [ ] `swag init` runs clean (no warnings)
- [ ] `docs/` is committed and up-to-date with handlers
- [ ] Every handler has `@Summary`, `@Router`, and at least one `@Success`
- [ ] Every protected handler has `@Security`
- [ ] Request bodies are named structs (no `map`, no primitives)
- [ ] Generic / nested response wrappers are spelled correctly (`Response[T]` or `Response{data=T}`)
- [ ] `/swagger/*` is gated in production (env flag or auth middleware)
- [ ] CI fails when `docs/` drifts from annotations

## References

- [references/swag-cli.md](references/swag-cli.md) — CLI flags, parsing options, Makefile patterns
- [references/annotations.md](references/annotations.md) — full annotation grammar with examples
- [references/struct-tags.md](references/struct-tags.md) — type overrides, time/UUID/decimal handling
- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each failure mode

Referenced files: 5

go-testing9.05 KB

View saved version →

---
name: go-testing
description: "Use when writing or fixing Go tests — table-driven cases, parallel safety, helpers, fakes, fuzzing, deterministic time (testing/synctest), goroutine leak detection (goleak), HTTP handlers. Apply proactively when a function gets a new test or a test is flaky. Benchmark methodology: see go-benchmark."
license: MIT
compatibility: "Designed for Claude Code or similar AI coding agents. Targets Go 1.21+. Uses Go 1.25+ testing/synctest and Go 1.24+ b.Loop() where relevant."
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
---

# Go Testing

Tests are executable specifications. Their job is to **fail usefully** when behaviour regresses — and to keep failing in the same way until the bug is fixed. Tests that are passing-or-flaky teach the team to ignore them, which is worse than no test at all.

## Core Rules

1. **Failures must be diagnosable from the log alone.** Every `t.Errorf` includes the function under test, the inputs, what we got, and what we wanted, in that order.
2. **No assertion libraries by default.** Use the standard `t.Errorf` / `t.Fatalf` plus `go-cmp` for structural comparison. `testify` is acceptable when adopted consistently — pick one and stick with it.
3. **Test observable behaviour, not implementation details.** If a refactor that preserves behaviour breaks the test, the test was wrong.
4. **Each test runs independently.** No execution-order dependencies, no shared global state without `t.Cleanup`.
5. **`t.Parallel()` whenever the test is safe to run in parallel.** Most are.
6. **`t.Helper()` is the first line of any helper function** that calls `t.Errorf`/`t.Fatalf`. Reserve `t.Fatal` for "next line is meaningless without this value"; everything else uses `t.Error`. Never call `t.Fatal`/`t.FailNow` from a non-test goroutine — send the failure back via channel.

## "Useful Failures" Format

The failure message is the test's user interface. The canonical shape:

```
FunctionUnderTest(input) = got, want want
```

```go
// Good
t.Errorf("Add(2, 3) = %d, want %d", got, 5)

// Bad — no function, no inputs, reversed
t.Errorf("expected %d but got %d", 5, got)
```

Always print **got before want**. With `cmp.Diff(want, got)`, the diff shows `(-want +got)` — echo that direction in your message.

## "No Asserts" Philosophy

Standard-library testing with `if` + `t.Errorf` reads as plain Go and produces messages you control. Assertion libraries shorten call sites but trade away message quality and reorder the `got`/`want` convention.

- **Default** — `if got != want { t.Errorf("...") }` plus `cmp.Diff` for structs, slices, maps, protos.
- **Permitted** — `testify` if the team agrees and `testifylint` is enabled. `require` only when continuing is meaningless.
- **Avoid** — mixing styles within the same package.

For protocol buffers, add `protocmp.Transform()` as a `cmp` option. Don't diff serialised JSON strings — decode and `cmp.Diff` instead.

## t.Error vs t.Fatal

Use `t.Error` by default; reserve `t.Fatal` for "the next line is meaningless without this value" (failed setup, failed decode before use). **Never** call `t.Fatal`/`t.FailNow` from a goroutine other than the test goroutine — it does not stop the test. Send the failure back via channel.

> Read [references/assertions-and-helpers.md](references/assertions-and-helpers.md) when designing helpers, custom comparers, or migrating between stdlib testing and `testify`.

## Table-Driven Tests

```go
func TestCalculatePrice(t *testing.T) {
    tests := []struct {
        name      string
        quantity  int
        unitPrice float64
        want      float64
    }{
        {"single item",   1,   10.0, 10.0},
        {"bulk discount", 100, 10.0, 900.0},
        {"zero quantity", 0,   10.0, 0.0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            got := CalculatePrice(tt.quantity, tt.unitPrice)
            if got != tt.want {
                t.Errorf("CalculatePrice(%d, %.2f) = %.2f, want %.2f",
                    tt.quantity, tt.unitPrice, got, tt.want)
            }
        })
    }
}
```

Every case has a `name` used in `t.Run`; failure messages include inputs, not the row index. When cases need different mocks or assertion shapes, stop using a table and write separate functions.

## Helpers, Cleanup, Parallel

```go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil { t.Fatalf("open db: %v", err) }
    t.Cleanup(func() { _ = db.Close() })
    return db
}
```

`t.Helper()` is the first line of any helper that may fail; `t.Cleanup` runs after the test (and subtests) in LIFO order. Call `t.Parallel()` inside the subtest function. The `paralleltest` linter catches missing calls and the pre-1.22 loop-variable trap.

## HTTP Handlers

Use `httptest` with table-driven cases. See [references/http-and-fakes.md](references/http-and-fakes.md) for request/response body, header, and status assertions.

## Goroutine Leaks: goleak

Wire `go.uber.org/goleak` into every package that spawns goroutines:

```go
import "go.uber.org/goleak"

func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
```

Per-test: `defer goleak.VerifyNone(t)`. Exclusions go to `goleak.IgnoreTopFunction(...)` — avoid `IgnoreAnyFunction`.

## Deterministic Time: testing/synctest

For timer/context/deadline tests, `testing/synctest` (Go 1.25+) gives reproducible ordering. Synthetic time advances only when every goroutine in the bubble is blocked:

```go
synctest.Test(t, func(t *testing.T) {
    ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
    defer cancel()
    time.Sleep(5 * time.Second)
    synctest.Wait()
    if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
        t.Fatalf("got %v, want DeadlineExceeded", ctx.Err())
    }
})
```

Use `synctest.Test` on Go 1.25+ and 1.26+. The Go 1.24 `GOEXPERIMENT=synctest` `synctest.Run` API is only for modules still on 1.24.

## Fuzzing and Benchmarks

Native fuzzing finds inputs you would never write by hand. Seed with `f.Add`, then `f.Fuzz`; check fuzzer-discovered corpora (`testdata/fuzz/...`) into git as regression tests.

Benchmarks use `for b.Loop()` on Go 1.24+ (the legacy `b.N` loop only when targeting older versions). Sub-benchmarks across sizes use `b.Run(fmt.Sprintf("n=%d", n), ...)`. For methodology, `benchstat`, and CI regression detection, see a dedicated `go-benchmark` skill — not this one.

> Read [references/fuzz-synctest-bench.md](references/fuzz-synctest-bench.md) when wiring fuzz corpora into CI, designing `synctest`-based timer tests, or writing comparable benchmark suites with `b.Loop`.

## Integration Tests

Separate by build tag so `go test ./...` stays fast:

```go
//go:build integration

package mypackage_test
```

Run with `go test -tags=integration ./...`. Integration tests own their fixtures (containers, schemas, fixtures) via `t.Cleanup`.

> Read [references/http-and-fakes.md](references/http-and-fakes.md) when writing HTTP handler tests, mocking via consumer-owned interfaces, or stubbing time.

## Anti-Patterns

| Anti-pattern | Do this instead |
|---|---|
| `t.Errorf("got %d", got)` without the function or wanted value | `FunctionUnderTest(input) = got, want want` |
| Comparing error strings (`err.Error() == "..."`) | `errors.Is` / `errors.As` |
| Calling `t.Fatal` from a spawned goroutine | Send via channel; main goroutine calls `t.Fatal` |
| Tables of cases with conditional setup per row | Split into separate test functions |
| Subtests without `t.Run(tt.name, ...)` | Always name and `t.Run` |
| Mocking concrete types from another package | Define a small interface in the consumer; pass a fake |
| `time.Sleep` to wait for "the goroutine to do its thing" | `synctest.Test` or explicit synchronisation |
| Snapshot tests that compare serialised JSON | Decode then `cmp.Diff` |
| Mutating `os.Args`/env/`flag.CommandLine` without `t.Cleanup` | Save and restore in `t.Cleanup` |

## Verification Checklist

- [ ] Every failure message includes function, inputs, got, and want, in that order
- [ ] `cmp.Diff` calls use `(-want +got)` order and echo it in the message
- [ ] Table-driven cases have `name` fields and use `t.Run`
- [ ] Helpers call `t.Helper()` and use `t.Cleanup` for teardown
- [ ] Parallel-safe tests call `t.Parallel()`; `paralleltest` is clean
- [ ] Packages that spawn goroutines wire `goleak.VerifyTestMain` or per-test `VerifyNone`
- [ ] Timer/deadline tests use `testing/synctest`, not `time.Sleep`
- [ ] Integration tests are gated by a build tag and own their fixtures
- [ ] `go test -race ./...` is clean; fuzz corpora are checked into `testdata/fuzz/...`

## References

- [references/assertions-and-helpers.md](references/assertions-and-helpers.md) — useful failures, `cmp.Diff`, helpers, `testify` interop
- [references/http-and-fakes.md](references/http-and-fakes.md) — `httptest`, consumer-owned fakes, time stubs
- [references/fuzz-synctest-bench.md](references/fuzz-synctest-bench.md) — fuzzing, `testing/synctest`, `b.Loop` benchmarks
- [references/goleak-and-flakes.md](references/goleak-and-flakes.md) — leak detection, flake diagnosis, race triage

Referenced files: 5

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
muratmirgun
Keywords
See publisher keywords

Declared capabilities

  • Read
  • Write

Package observed Oct 3, 2026.

Technical details
First seen
Sep 30, 2026 · 22:02 UTC
Last seen
Oct 3, 2026 · 12:00 UTC
Collection status
Collected

plugins_6a7b1e3e30948191aea92f131b0f6ca9

Download plugin data (JSON)

Before you connect Gophers

How do I connect it?

Open the publisher's marketplace listing to check current availability and follow its connection instructions. This directory does not install plugins. Check the requested access and any account requirements before connecting.

Check marketplace availability ↗

Does it require paid access?

We have not established the pricing or subscription requirements for this plugin. An absent price does not mean free access.

Compare researched pricing and access models →

How can I evaluate it?

Check the declared skills and available files, then try a small task whose result you can verify. Our archived descriptions and instructions establish publisher claims, not tested runtime quality. Review sources and coverage limits.