Go
Go services and tools.
What this is
Go services and tools. Its rules cluster tightly around three things Go makes easy to get wrong: error handling (ignored errors, panicking for expected failures instead of returning them), goroutine and context lifetime (leaked goroutines, missing context propagation, data races, WaitGroup misuse), and resource discipline (defer piling up in loops, HTTP clients without timeouts, response bodies left undrained).
How to onboard it
A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:
$ npx redlinegate init # detects the profile $ npx redlinegate init --profile service-go # or name one
One profile pulls these rules in: service-go.
Once onboarded, your files match this stack when they fit any of these globs:
**/*.go
How to use it — 20 rules
Nothing to run. Once your profile includes Go, redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.
| Rule id | Severity | Catches |
|---|---|---|
go/ignored-errors | BLOCKER | Ignored errors. `_ = err`, unchecked returns, or `err` shadowed and never handled. Every error is handled, returned wrapped (`fmt.Errorf("...: %w", err)`), or explicitly justified. |
go/goroutine-leaks | BLOCKER | Goroutine leaks. Goroutine started without a way to stop: missing context cancellation, blocked forever on channel send/receive, no `WaitGroup`/errgroup ownership. |
go/missing-ctx-propagation | BLOCKER | Missing `ctx` propagation. Outbound calls (HTTP, DB, gRPC) must take the request's `context.Context`; no `context.Background()` inside request handling. |
go/data-races | BLOCKER | Data races. Shared map/slice/struct written from multiple goroutines without mutex or channel ownership; loop variable captured by reference in goroutine (pre-1.22 semantics or when version unknown). |
go/writing-nil-map | BLOCKER | Writing to a nil map. |
go/copying-sync-primitive | BLOCKER | Copying a struct containing `sync.Mutex`/`sync.WaitGroup` (passing by value, range over slice of them). |
go/defer-in-loop | BLOCKER | `defer` in a loop for per-iteration resources (files, rows, locks) — defers pile up until function exit; extract loop body into a function. |
go/panic-for-expected-failure | BLOCKER | Panics for expected failures — panic only for programmer errors; return errors otherwise. |
go/missing-client-server-timeout | HIGH | `http.Client`/`http.Server` without timeouts (`Timeout`, `ReadTimeout`, `WriteTimeout`) — zero values mean infinite. |
go/response-body-not-drained | HIGH | Response body not closed, or closed without being drained (breaks connection reuse). |
go/errors-is-as | HIGH | `errors.Is`/`errors.As` not used where sentinel/typed errors are compared with `==` or type assertion. |
go/interface-at-implementation | HIGH | Interfaces defined next to the implementation instead of the consumer; interfaces with one implementation and no test need. |
go/waitgroup-add-inside-goroutine | HIGH | `sync.WaitGroup.Add` inside the goroutine instead of before starting it. |
go/unbuffered-channel-blocks-producer | HIGH | Unbuffered channel used where the producer must never block, or buffer sizes chosen arbitrarily without comment. |
go/time-after-in-loop | HIGH | `time.After` in a loop (leaks timers until fire) — use `time.NewTimer`/`Ticker` with Stop. |
go/package-level-mutable-state | HIGH | Package-level mutable state in new code. |
go/naked-returns | SUGGESTION | Naked returns in functions longer than a few lines. |
go/any-over-concrete-type | SUGGESTION | `interface{}`/`any` where a concrete type or generic works. |
go/error-string-style | SUGGESTION | Error strings capitalized or ending with punctuation (Go convention: lowercase, no period). |
go/receiver-consistency | SUGGESTION | Struct field alignment/pointer-vs-value receiver inconsistency within a type. |
Expected output
A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:
Redline/BLOCKER [go/ignored-errors]: <one-line problem>Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 20 rules here, 8 BLOCKER, 8 HIGH and 4 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.
If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.
How to edit it
Rules are edited in standards/stacks/go.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.
- Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
- node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
- node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
- Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
- Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
- node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.
Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.
The full file
# Go Review Rules
## BLOCKER — request changes
- `go/ignored-errors` — **Ignored errors.** `_ = err`, unchecked returns, or `err` shadowed and never handled. Every error is handled, returned wrapped (`fmt.Errorf("...: %w", err)`), or explicitly justified.
- `go/goroutine-leaks` — **Goroutine leaks.** Goroutine started without a way to stop: missing context cancellation, blocked forever on channel send/receive, no `WaitGroup`/errgroup ownership.
- `go/missing-ctx-propagation` — **Missing `ctx` propagation.** Outbound calls (HTTP, DB, gRPC) must take the request's `context.Context`; no `context.Background()` inside request handling.
- `go/data-races` — **Data races.** Shared map/slice/struct written from multiple goroutines without mutex or channel ownership; loop variable captured by reference in goroutine (pre-1.22 semantics or when version unknown).
- `go/writing-nil-map` — **Writing to a nil map.**
- `go/copying-sync-primitive` — **Copying a struct containing `sync.Mutex`/`sync.WaitGroup`** (passing by value, range over slice of them).
- `go/defer-in-loop` — **`defer` in a loop for per-iteration resources** (files, rows, locks) — defers pile up until function exit; extract loop body into a function.
- `go/panic-for-expected-failure` — **Panics for expected failures** — panic only for programmer errors; return errors otherwise.
## HIGH
- `go/missing-client-server-timeout` — `http.Client`/`http.Server` without timeouts (`Timeout`, `ReadTimeout`, `WriteTimeout`) — zero values mean infinite.
- `go/response-body-not-drained` — Response body not closed, or closed without being drained (breaks connection reuse).
- `go/errors-is-as` — `errors.Is`/`errors.As` not used where sentinel/typed errors are compared with `==` or type assertion.
- `go/interface-at-implementation` — Interfaces defined next to the implementation instead of the consumer; interfaces with one implementation and no test need.
- `go/waitgroup-add-inside-goroutine` — `sync.WaitGroup.Add` inside the goroutine instead of before starting it.
- `go/unbuffered-channel-blocks-producer` — Unbuffered channel used where the producer must never block, or buffer sizes chosen arbitrarily without comment.
- `go/time-after-in-loop` — `time.After` in a loop (leaks timers until fire) — use `time.NewTimer`/`Ticker` with Stop.
- `go/package-level-mutable-state` — Package-level mutable state in new code.
## SUGGESTION
- `go/naked-returns` — Naked returns in functions longer than a few lines.
- `go/any-over-concrete-type` — `interface{}`/`any` where a concrete type or generic works.
- `go/error-string-style` — Error strings capitalized or ending with punctuation (Go convention: lowercase, no period).
- `go/receiver-consistency` — Struct field alignment/pointer-vs-value receiver inconsistency within a type.