# Go Style Guide

> Conventions for writing, structuring, testing and shipping Go 1.27 services, agents and CLIs that stay simple, explicit and easy to read.

Revision 2026.10. This URL always serves the latest text; pin a revision with https://style.chopper.dev/go@2026.10/llms.txt. Changelog: https://style.chopper.dev/go/changelog.md.

Rule levels: MUST / MUST NOT (non-negotiable), SHOULD / SHOULD NOT (the default; deviate only with a short comment saying why), MAY (allowed when it helps), AVOID (allowed but a smell). In this document they appear as a bracketed prefix on each rule, after its stable ID, e.g. "`GO-02.1` [MUST] ...". Cite a rule by its ID (GO-02.1): IDs never change or get reused, and https://style.chopper.dev/go#GO-02.1 links to the rule. The guide is generic. Examples use the module path `example.com/project`, the binary `projectd` and an `invoice` domain; swap in your own.

## Starter project

A ready-to-run invoice service with the guide's cmd/internal layout, a pgx and sqlc PostgreSQL store with goose migrations, a stdlib HTTP API with problem+json errors, slog logging, Prometheus RED metrics and pprof on an internal listener, graceful shutdown, the appendix golangci-lint and Taskfile configs, table, synctest, fuzz and integration tests, Docker, GoReleaser and CI. It is generated from this guide and follows it, so prefer it over scaffolding a new codebase by hand.

Download it as a zip, renamed to the user's names, with a GET request (query parameters are optional; missing ones take their defaults, invalid ones return HTTP 400 with a message):

```bash
curl -fsSL "https://style.chopper.dev/go/template/download?name=Project" -o project.zip && unzip project.zip
```

Parameters:

- `name`: Project name. PascalCase. Lowercased, it names the binary (name + d) and the default module path. Must match `^(?:[A-Z][A-Za-z0-9]{1,31})$`. Default `Project`.
- `module`: Module path. Where the module will live, e.g. github.com/you/invoicer. Defaults to example.com/<name>. Must match `^(?:[a-z0-9.-]+(/[A-Za-z0-9._~-]+)+)$`.

After unzipping:

1. **Install.** Install Go 1.27, Task and Docker; every other tool runs through go tool from go.mod, nothing global.
2. **Run.** Run task lint and task test, then docker compose -f deploy/compose.yaml up --build to serve on :8080.

## Contents

- 01. Purpose and how to read this guide
- 02. Baseline and toolchain
- 03. Project layout
- 04. Naming
- 05. Formatting and linting
- 06. Packages, APIs and interfaces
- 07. Types, structs, generics and zero values
- 08. Error handling
- 09. Functions, methods and control flow
- 10. Context and cancellation
- 11. Concurrency
- 12. Logging and observability
- 13. Configuration, CLIs and startup
- 14. HTTP services
- 15. Data access
- 16. Agents and daemons
- 17. Performance and memory
- 18. Security
- 19. Testing
- 20. Documentation and comments
- 21. Git, commits, releases and review
- A. Appendix

## 01. Purpose and how to read this guide

Every Go project that adopts this guide — services, CLIs, agents and daemons, libraries — is written so that any two projects read as if one person wrote them. It builds on [Effective Go](https://go.dev/doc/effective_go), the [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) and the [Google Go Style Guide](https://google.github.io/styleguide/go/), and adds decisions those leave open.

The guide is generic. Examples use the module path `example.com/project`, the binary `projectd` and an `invoice` domain; swap in your own.

### Rule levels

| Level | Meaning |
| --- | --- |
| [MUST] / [MUST NOT] | Required. CI or review blocks the change. |
| [SHOULD] / [SHOULD NOT] | The default. Deviate only with a short comment explaining why. |
| [MAY] | Allowed when it helps; no justification needed. |
| [AVOID] | Legal but discouraged; expect a review question. |

### Principles

- **Clarity first.** The reader's time matters more than the writer's. Clear beats clever; obvious beats short.
- **Simplicity.** Use the least machinery that solves the problem: a function before a type, a type before an interface, the standard library before a dependency.
- **Consistency.** Match the surrounding code, then this guide, then the wider Go community.
- **Let the tools decide.** Anything `gofmt`, `go vet`, `go fix` or `golangci-lint` can check is not debated in review.
- **Errors are values.** Handle them where they happen, add context, and never hide them.
- **Measure before optimizing.** Profile first; keep the fast path readable.

## 02. Baseline and toolchain

| Area | Standard |
| --- | --- |
| Language | Go **1.27** (August 2026): generic methods, `encoding/json/v2`, standard `uuid` package |
| Previous release | Go 1.26 remains supported until Go 1.28 ships; libraries MAY target it |
| Modules | One module per repository by default; `go.work` only for local multi-module work |
| Formatting | `gofmt` (via `gofumpt`) + `goimports` |
| Linting | `go vet` + `golangci-lint` v2 with the committed config (Appendix A) |
| Modernizing | `go fix` modernizers, run before each release |
| Vulnerabilities | `govulncheck` in CI |
| Tests | `go test -race -shuffle=on`; `testing/synctest` for time and concurrency |
| Tasks | `Taskfile.yml` or a `Makefile` with standard targets (Appendix B) |
| Release | GoReleaser for binaries and container images |

### Rules

- `GO-02.1` [MUST] declare the language version with a `go 1.27.0` line in `go.mod`, and pin the toolchain with a `toolchain` line so every machine and CI builds with the same compiler.
- `GO-02.2` [MUST] commit `go.sum`. [MUST NOT] commit a `vendor/` directory unless the build environment has no module proxy.
- `GO-02.3` [MUST] manage developer tools with `go tool` (the `tool` directive in `go.mod`), not with a `tools.go` file or global installs: `go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint`.
- `GO-02.4` [SHOULD] upgrade to each new Go release within one minor release, running `go fix ./...` to apply the modernizers.
- `GO-02.5` [MUST NOT] add a dependency for something the standard library does well (HTTP routing, structured logging, UUIDs, JSON, slices and maps helpers).
- `GO-02.6` [SHOULD] prefer small, well-maintained dependencies; every new direct dependency is called out in the pull request description.
- `GO-02.7` [MUST] build release binaries with `CGO_ENABLED=0` unless the project needs cgo, and with `-trimpath` and version information injected (§21).

## 03. Project layout

Start flat and add structure only when a package earns it. A small CLI can be a single `main` package; the layout below is for services and agents.

```text
project/
  cmd/
    projectd/           # One directory per binary; main.go only wires things up
      main.go
    projectctl/
      main.go
  internal/             # Everything not meant for other modules
    invoice/            # Domain package: types, rules, service
      invoice.go
      invoice_test.go   # Tests live beside the code they test
      service.go
      service_test.go
      store.go          # Interface the domain needs from storage
    postgres/           # Storage implementation (sqlc-generated + wrappers)
      queries/
      invoice_store.go
    httpapi/            # HTTP handlers, routing, middleware
    agent/              # Long-running agent loop (if any)
    config/
    platform/           # Cross-cutting: logging, metrics, shutdown
  migrations/           # Numbered SQL migrations
  api/                  # OpenAPI specs, protobuf files
  deploy/               # Dockerfile, systemd units, Helm/compose
  scripts/
  testdata/             # Fixtures shared by tests
  go.mod
  go.sum
  Taskfile.yml
  .golangci.yml
```

### Rules

- `GO-03.1` [MUST] put each binary in `cmd/<name>/`, with `main.go` limited to parsing config, building dependencies and calling `run(ctx)`. All logic lives in `internal/`.
- `GO-03.2` [MUST] put code in `internal/` unless another module is meant to import it. [MUST NOT] create a `pkg/` directory by default; a public package lives at the module root or under a named directory and is documented as public API.
- `GO-03.3` [MUST] keep tests beside the code they test, in `<name>_test.go`. Black-box tests use the `<pkg>_test` package; white-box tests use the same package.
- `GO-03.4` [MUST] organise packages by **what they provide** (domain or capability: `invoice`, `postgres`, `httpapi`), not by kind (`models`, `controllers`, `utils`).
- `GO-03.5` [MUST NOT] create `util`, `common`, `helpers`, `misc` or `base` packages.
- `GO-03.6` [MUST NOT] create import cycles; when two packages need each other, move the shared types into the domain package or define a small interface on the consumer side.
- `GO-03.7` [SHOULD] keep test fixtures in a `testdata/` directory next to the test (the go tool ignores it).
- `GO-03.8` [SHOULD] keep generated code in its own files ending in `.gen.go` or under a `gen/` directory, with the generator command in a `//go:generate` line.

## 04. Naming

| Thing | Convention | Example |
| --- | --- | --- |
| Package | Short, lower case, one word, no underscores | `invoice`, `httpapi`, `postgres` |
| File | Lower case, underscores allowed | `invoice_store.go`, `service_test.go` |
| Exported identifier | MixedCaps | `NewService`, `MaxRetries` |
| Unexported identifier | mixedCaps | `parseLine`, `defaultTimeout` |
| Initialism | All one case | `ID`, `URL`, `HTTPClient`, `userID`, `apiURL` |
| Interface (one method) | Method + `er` | `Reader`, `Notifier` |
| Receiver | 1–2 letters, consistent per type | `func (s *Service)` |
| Constructor | `New` / `New<Type>` | `invoice.NewService` |
| Getter | No `Get` prefix | `inv.Total()`, not `inv.GetTotal()` |
| Error variable | `Err` prefix | `ErrNotFound` |
| Error type | `Error` suffix | `ValidationError` |
| Test | `Test<Func>_<case>` or subtests | `TestParse`, `t.Run("empty input", ...)` |
| Context variable | `ctx`, first parameter | `func Do(ctx context.Context, ...)` |
| Constants | MixedCaps, never UPPER_SNAKE | `defaultPort` |

### Rules

- `GO-04.1` [MUST] avoid stutter: the package name is part of every call site. `invoice.Service`, not `invoice.InvoiceService`; `postgres.NewStore`, not `postgres.NewPostgresStore`.
- `GO-04.2` [MUST] keep names short in proportion to their scope: `i`, `n`, `r` in a three-line loop; descriptive names for package-level identifiers.
- `GO-04.3` [MUST NOT] use `this` or `self` as a receiver name, and [MUST] use the same receiver name in every method of a type.
- `GO-04.4` [MUST NOT] shadow package names or built-ins (`len`, `new`, `error`, `url`) with local variables.
- `GO-04.5` [SHOULD] name booleans as states or questions: `enabled`, `isLeader`, `hasLines`.
- `GO-04.6` [SHOULD] name functions that return a boolean as questions (`IsOverdue`), and functions with side effects as verbs (`Send`, `Close`).
- `GO-04.7` [AVOID] naming a package after its pattern (`manager`, `handler`, `service`); name it after what it manages.

## 05. Formatting and linting

Formatting is never a review topic: the tools decide, CI enforces.

### Formatting

- `GO-05.1` [MUST] format every file with `gofumpt` (a stricter `gofmt`) and group imports with `goimports -local example.com/project`. CI fails on any diff.
- `GO-05.2` [MUST] indent with **tabs**, as `gofmt` does. This is the one place the 2-space rule from other guides does not apply; Go's formatter is not configurable and that is the point.
- `GO-05.3` [MUST] group imports in three blocks: standard library, third-party, then this module.
- `GO-05.4` [SHOULD] keep lines under about **100** characters. There is no hard limit; break a long call one argument per line with a trailing comma rather than leaving a 200-character line.
- `GO-05.5` [MUST NOT] use dot imports outside tests, or blank imports anywhere but `main` and tests, where database drivers are registered.
- `GO-05.6` [SHOULD] order a file: package doc, imports, constants, variables, types (each followed by its constructor and methods), then free functions.

### Static analysis

- `GO-05.7` [MUST] run `go vet ./...` and `golangci-lint run` in CI with the committed `.golangci.yml` (Appendix A). The build fails on any finding.
- `GO-05.8` [MUST NOT] silence a linter without a reason: `//nolint:gosec // path is from a constant, not user input`. Blanket `//nolint` is forbidden.
- `GO-05.9` [MUST] run `govulncheck ./...` in CI and on a schedule.
- `GO-05.10` [SHOULD] run `go fix ./...` (the Go 1.26+ modernizers) when upgrading Go, and commit the result in its own `refactor:` commit.
- `GO-05.11` [SHOULD] add `//go:fix inline` to deprecated wrapper functions in public packages so callers can migrate automatically.

### One command

- `GO-05.12` [MUST] expose `task lint` and `task test` (or `make lint` / `make test`), and run exactly those in CI (Appendix B).

## 06. Packages, APIs and interfaces

### Package design

- `GO-06.1` [MUST] give each package one clear purpose that its name states; if the package doc needs "and", consider splitting it.
- `GO-06.2` [MUST] keep the exported surface small. Export a name only when another package uses it today.
- `GO-06.3` [MUST NOT] use package-level mutable state (globals, `init()` side effects) beyond registering drivers or metrics. Pass dependencies explicitly.
- `GO-06.4` [AVOID] `init()` functions; when one is unavoidable, it must not do I/O or depend on flags or environment.
- `GO-06.5` [SHOULD] keep a package's dependencies pointing inward: `httpapi` and `postgres` import `invoice`; `invoice` imports neither.

### Interfaces

- `GO-06.6` [MUST] define interfaces in the package that **uses** them, not the package that implements them. Producers return concrete types.
- `GO-06.7` [MUST] keep interfaces small — usually one to three methods — and declare only the methods the consumer calls.
- `GO-06.8` [MUST NOT] create an interface for a single implementation "for testing" when a real or in-memory implementation would do. Introduce the interface when the second implementation or a test double is actually needed.
- `GO-06.9` [SHOULD] follow "accept interfaces, return structs".
- `GO-06.10` [SHOULD] assert that a type satisfies an interface at compile time where the relationship matters: `var _ invoice.Store = (*Store)(nil)`.
- `GO-06.11` [MUST NOT] embed an interface in a struct to "implement" it partially; a nil method call panics at runtime.

```go
// Package invoice holds invoice rules and the service that applies them.
package invoice

// Store is what the invoice service needs from persistence.
type Store interface {
	Get(ctx context.Context, id InvoiceID) (Invoice, error)
	Save(ctx context.Context, inv Invoice) error
}

// Service applies invoice rules.
type Service struct {
	store Store
	clock func() time.Time
	log   *slog.Logger
}

// NewService returns a Service backed by store.
func NewService(store Store, log *slog.Logger) *Service {
	return &Service{store: store, clock: time.Now, log: log}
}
```

### Compatibility

- `GO-06.12` [MUST] follow semantic versioning for any module others import, and bump the major version (`/v2` module path) for breaking changes.
- `GO-06.13` [MUST] mark replaced APIs with a `// Deprecated:` paragraph naming the replacement, and keep them for at least one minor release.

## 07. Types, structs, generics and zero values

### Structs and zero values

- `GO-07.1` [SHOULD] make the zero value useful: a `sync.Mutex`, `bytes.Buffer` or `Config` with sensible defaults needs no constructor. When the zero value cannot be valid, provide `New...` and document that the zero value must not be used.
- `GO-07.2` [MUST] use field names in struct literals for types from other packages: `http.Server{Addr: addr}`, never positional.
- `GO-07.3` [MUST] use pointer receivers when a method mutates the receiver, when the struct holds a `sync.Mutex` or similar, or when it is large; otherwise value receivers. [MUST NOT] mix receiver kinds on one type without reason.
- `GO-07.4` [MUST NOT] copy a struct that contains a mutex, `sync.WaitGroup` or `atomic` value (`go vet` copylocks enforces this).
- `GO-07.5` [SHOULD] use `new(expr)` (Go 1.26) for optional pointer fields instead of a temporary variable: `Age: new(yearsSince(born))`.
- `GO-07.6` [SHOULD] use typed IDs and units to prevent mix-ups: `type InvoiceID uuid.UUID`, `type Cents int64`.
- `GO-07.7` [MUST] store money as integer minor units (`int64` cents) with a currency code, never as `float64`.
- `GO-07.8` [MUST] use UUIDv7 for generated identifiers so they sort by creation time, using the standard `uuid` package (Go 1.27) rather than a third-party module.

### Enums

- `GO-07.9` [MUST] declare enumerations as a named type with `iota` constants, starting from a meaningful zero (`StatusUnknown`) so an unset value is detectable.
- `GO-07.10` [SHOULD] generate `String()` with `stringer` and implement `encoding.TextMarshaler` / `TextUnmarshaler` so the value round-trips through JSON and config as text.
- `GO-07.11` [MUST] handle every case in a `switch` over an enum and return an error (or panic in truly impossible cases) in `default`; the `exhaustive` linter enforces this.

```go
type Status int

const (
	StatusUnknown Status = iota
	StatusDraft
	StatusSent
	StatusPaid
	StatusOverdue
)

//go:generate go tool stringer -type=Status -trimprefix=Status

func (s Status) IsOpen() bool {
	return s == StatusSent || s == StatusOverdue
}
```

### Generics

- `GO-07.12` [SHOULD] reach for generics when the same code would otherwise be written for several types (containers, `slices`/`maps`-style helpers, typed caches, result sets). [AVOID] generics for code that has one caller.
- `GO-07.13` [MUST] prefer the standard `slices`, `maps`, `cmp` and `iter` packages to hand-written loops or third-party helpers.
- `GO-07.14` [MAY] use generic methods (Go 1.27) when a method genuinely needs its own type parameter; remember that interface methods cannot be generic, so such a method can never satisfy an interface.
- `GO-07.15` [SHOULD] keep constraints as loose as the code allows (`any`, `comparable`, `cmp.Ordered`) and name type parameters with one capital letter or a short word (`T`, `K`, `V`, `Item`).
- `GO-07.16` [SHOULD] return iterators (`iter.Seq[T]`, `iter.Seq2[K, V]`) from collections or paged sources that callers range over, instead of building a full slice.

```go
// All yields every invoice for the customer, fetching pages lazily.
func (s *Store) All(ctx context.Context, customer CustomerID) iter.Seq2[Invoice, error] {
	return func(yield func(Invoice, error) bool) {
		for page, err := range s.pages(ctx, customer) {
			if err != nil {
				yield(Invoice{}, err)
				return
			}
			for _, inv := range page {
				if !yield(inv, nil) {
					return
				}
			}
		}
	}
}
```

## 08. Error handling

### Handling and wrapping

- `GO-08.1` [MUST] check every returned error. [MUST NOT] discard one with `_` unless a comment says why it cannot matter.
- `GO-08.2` [MUST] handle an error once: either log it **or** return it, never both. The top of the call stack (the handler, the job runner, `main`) logs.
- `GO-08.3` [MUST] add context when returning an error from another package, using `%w`: `fmt.Errorf("load invoice %s: %w", id, err)`. Context describes what **this** function was doing, in lower case, with no "failed to" or "error" prefix.
- `GO-08.4` [MUST] keep the happy path left-aligned: handle the error and return early; no `else` after a `return`.
- `GO-08.5` [SHOULD] use `%v` instead of `%w` when callers must not depend on the underlying error (for example, hiding a driver error behind a domain boundary).
- `GO-08.6` [MUST NOT] wrap a pointer to an error value type (`*MyErr` when `MyErr` implements `error`); Go 1.27's vet flags it because `errors.Is` and type checks stop working.

### Inspecting errors

- `GO-08.7` [MUST] compare errors with `errors.Is` and extract them with `errors.AsType[T]` (Go 1.26), never with `==` on wrapped errors or string matching.
- `GO-08.8` [SHOULD] expose **sentinel errors** (`var ErrNotFound = errors.New("invoice: not found")`) for conditions callers branch on, and **error types** when callers need fields.
- `GO-08.9` [MUST] translate storage errors into domain errors at the boundary: `postgres` turns `pgx.ErrNoRows` into `invoice.ErrNotFound`.
- `GO-08.10` [SHOULD] use `errors.Join` to return several independent errors (validation, closing multiple resources).

```go
var ErrNotFound = errors.New("invoice: not found")

type ValidationError struct {
	Field  string
	Reason string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("invoice: %s %s", e.Field, e.Reason)
}

func (s *Service) MarkPaid(ctx context.Context, id InvoiceID) error {
	inv, err := s.store.Get(ctx, id)
	if err != nil {
		return fmt.Errorf("mark paid %s: %w", id, err)
	}
	if inv.Status == StatusPaid {
		return &ValidationError{Field: "status", Reason: "is already paid"}
	}

	inv.Status = StatusPaid
	inv.PaidAt = s.clock()

	if err := s.store.Save(ctx, inv); err != nil {
		return fmt.Errorf("mark paid %s: save: %w", id, err)
	}
	return nil
}

// In the HTTP layer:
switch err := svc.MarkPaid(ctx, id); {
case err == nil:
	w.WriteHeader(http.StatusNoContent)
case errors.Is(err, invoice.ErrNotFound):
	writeProblem(w, http.StatusNotFound, "invoice not found")
default:
	if verr, ok := errors.AsType[*invoice.ValidationError](err); ok {
		writeProblem(w, http.StatusUnprocessableEntity, verr.Error())
		return
	}
	writeInternalError(w, r, err)
}
```

### Panics

- `GO-08.11` [MUST NOT] panic for expected failures (bad input, missing rows, network errors). Return an error.
- `GO-08.12` [MAY] panic for programmer errors that make continuing unsafe (an impossible state, a broken invariant), and in `Must...` helpers used only at startup with constant input (`regexp.MustCompile`).
- `GO-08.13` [MUST] recover panics at goroutine and request boundaries (HTTP middleware, worker loops), log them with the stack, and turn them into errors. [MUST NOT] use `recover` for control flow.
- `GO-08.14` [MUST NOT] call `log.Fatal` or `os.Exit` outside `main`; they skip deferred cleanup.

## 09. Functions, methods and control flow

### Signatures

- `GO-09.1` [MUST] put `ctx context.Context` first and `error` last.
- `GO-09.2` [SHOULD] keep functions under about 60 lines and four parameters. When parameters grow, group them in a struct (`Params`, `Options`) or use functional options.
- `GO-09.3` [MUST NOT] use named results except to document two results of the same type or when a deferred function must set the error. [MUST NOT] use naked `return` in functions longer than a few lines.
- `GO-09.4` [MUST NOT] use boolean flag parameters that change behaviour (`Send(inv, true)`); use two functions or an options struct.
- `GO-09.5` [SHOULD] return a concrete value and an error, not a pointer to signal "maybe": `(Invoice, error)` with `ErrNotFound`, rather than `(*Invoice, error)` with `nil, nil`.
- `GO-09.6` [MUST NOT] return `nil, nil`.

### Options

- `GO-09.7` [SHOULD] use an options struct for constructors with several optional settings; it is easy to read, document and extend.
- `GO-09.8` [MAY] use functional options (`WithTimeout(d)`) for public APIs where defaults matter and the set of options will grow.

```go
type ClientOptions struct {
	Timeout    time.Duration // Default 10s.
	MaxRetries int           // Default 3; 0 disables retries.
	UserAgent  string
}

func NewClient(baseURL string, opts ClientOptions) *Client {
	if opts.Timeout == 0 {
		opts.Timeout = 10 * time.Second
	}
	// ...
}
```

### Control flow

- `GO-09.9` [MUST] return early to keep the main path at the lowest indentation.
- `GO-09.10` [SHOULD] use `switch` with no condition instead of long `if`/`else if` chains.
- `GO-09.11` [SHOULD] range over integers (`for i := range n`) and over functions (`for v := range seq`) instead of three-clause loops where it reads better.
- `GO-09.12` [MUST] use `defer` to release resources right after acquiring them, and check the error from `Close` on writers: `defer func() { err = errors.Join(err, f.Close()) }()`.
- `GO-09.13` [MUST NOT] `defer` inside a loop that runs many times; move the body into a function.
- `GO-09.14` [SHOULD] declare variables close to first use, and prefer `:=` except when the zero value is the point (`var buf bytes.Buffer`).
- `GO-09.15` [AVOID] `goto` and labelled `break`/`continue` except to leave a nested loop or `select`.

## 10. Context and cancellation

- `GO-10.1` [MUST] pass `context.Context` as the first parameter, named `ctx`, to every function that does I/O, blocks or calls something that does.
- `GO-10.2` [MUST NOT] store a context in a struct field. The only exception is a type that represents one request's lifetime and documents it.
- `GO-10.3` [MUST NOT] pass `nil` as a context. Use `context.TODO()` while migrating, and `context.Background()` only in `main`, tests and top-level goroutines.
- `GO-10.4` [MUST] derive the root context from signals in `main` with `signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)`; since Go 1.26 its cancel cause names the signal.
- `GO-10.5` [MUST] set deadlines on every outbound call: `context.WithTimeout` per call, or a client-level timeout. A call with no deadline is a bug waiting for a slow dependency.
- `GO-10.6` [SHOULD] use `context.WithCancelCause` / `WithTimeoutCause` and read `context.Cause(ctx)` so logs say **why** work stopped.
- `GO-10.7` [MUST] call the `cancel` function returned by `WithCancel`/`WithTimeout`, normally with `defer cancel()`.
- `GO-10.8` [SHOULD] use `context.WithoutCancel(ctx)` for work that must finish after the request ends (audit writes), and `context.AfterFunc` instead of a goroutine waiting on `ctx.Done()`.
- `GO-10.9` [MAY] carry request-scoped values (request ID, authenticated principal, trace span) in a context with unexported key types and typed accessors. [MUST NOT] use context values for optional parameters or dependencies.

```go
type ctxKey struct{}

func WithPrincipal(ctx context.Context, p Principal) context.Context {
	return context.WithValue(ctx, ctxKey{}, p)
}

func PrincipalFrom(ctx context.Context) (Principal, bool) {
	p, ok := ctx.Value(ctxKey{}).(Principal)
	return p, ok
}
```

## 11. Concurrency

### Goroutines

- `GO-11.1` [MUST] know how every goroutine ends before starting it: it stops when its context is cancelled, its input channel closes, or its work completes. A goroutine with no exit is a leak.
- `GO-11.2` [MUST] wait for goroutines you start before returning from the function or shutting down: `sync.WaitGroup.Go` (Go 1.25+) or `errgroup.Group`.
- `GO-11.3` [SHOULD] use `golang.org/x/sync/errgroup` with `errgroup.WithContext` for a set of tasks where the first error cancels the rest, and `g.SetLimit(n)` to bound parallelism.
- `GO-11.4` [MUST NOT] start unbounded goroutines per item (one per row, per request to a dependency); bound them with a limit or a worker pool.
- `GO-11.5` [MUST] recover panics in long-lived goroutines that are not covered by a request middleware, and report them.
- `GO-11.6` [SHOULD] check for leaks in tests with the `goroutineleak` profile (generally available in Go 1.27) or `go.uber.org/goleak`.

### Channels vs mutexes

- `GO-11.7` [SHOULD] use channels to pass ownership of data or signal events between goroutines, and a `sync.Mutex` to protect shared state. Pick the one that makes the code simpler.
- `GO-11.8` [MUST] close a channel only from the sending side, and only once; document which goroutine owns it.
- `GO-11.9` [SHOULD] keep channels unbuffered or size 1 unless a measured reason calls for more; a large buffer usually hides a back-pressure problem.
- `GO-11.10` [MUST] use `select` with `ctx.Done()` on every blocking send or receive that can outlive the caller.
- `GO-11.11` [MUST] keep the mutex next to the fields it guards, declared just above them, and keep critical sections short with no I/O inside.
- `GO-11.12` [SHOULD] use `atomic.Int64`, `atomic.Bool`, `atomic.Pointer[T]` (the typed atomics) instead of the `atomic.AddInt64` functions; `go fix` migrates old code.
- `GO-11.13` [SHOULD] use `sync.OnceValue` / `OnceValues` for lazy initialization.
- `GO-11.14` [MUST] run every test suite with `-race`; a race report is a bug, never flaky.

```go
func (s *Syncer) SyncAll(ctx context.Context, sites []Site) error {
	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(8)

	for _, site := range sites {
		g.Go(func() error {
			if err := s.syncSite(ctx, site); err != nil {
				return fmt.Errorf("sync site %s: %w", site.ID, err)
			}
			return nil
		})
	}
	return g.Wait()
}

type Cache struct {
	mu      sync.RWMutex
	entries map[string]Entry // guarded by mu
}
```

## 12. Logging and observability

### Logging with slog

- `GO-12.1` [MUST] log with `log/slog`. [MUST NOT] use `log.Printf`, `fmt.Println` or a third-party logger in new code.
- `GO-12.2` [MUST] inject a `*slog.Logger` into types that log; [AVOID] `slog.Default()` outside `main`.
- `GO-12.3` [MUST] log structured attributes, not formatted strings: `log.InfoContext(ctx, "invoice sent", "invoice_id", id, "attempt", n)`. Keys are `snake_case` and stable.
- `GO-12.4` [SHOULD] use the `...Context` methods so handlers can add request IDs and trace IDs from the context.
- `GO-12.5` [MUST] use levels consistently: `Debug` for developer detail, `Info` for business events and lifecycle, `Warn` for degraded but handled conditions, `Error` for failures someone must look at.
- `GO-12.6` [MUST] emit JSON (`slog.NewJSONHandler`) in production and text locally. Use `slog.NewMultiHandler` (Go 1.26) to send to more than one destination.
- `GO-12.7` [MUST NOT] log secrets, tokens, passwords or personal data; implement `slog.LogValuer` on sensitive types to redact them.
- `GO-12.8` [SHOULD] log an error once, at the boundary, with the full wrapped message under the `err` key.

```go
type APIKey string

func (APIKey) LogValue() slog.Value { return slog.StringValue("[redacted]") }

log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel}))
log = log.With("service", "projectd", "version", version.Version)
```

### Metrics and tracing

- `GO-12.9` [SHOULD] instrument services with OpenTelemetry (traces + metrics) or Prometheus metrics; pick one per project and use it everywhere.
- `GO-12.10` [MUST] export the RED metrics for every server (rate, errors, duration) and the USE metrics for every worker pool or queue (utilization, saturation, errors).
- `GO-12.11` [MUST] keep metric label values bounded: no IDs, emails or URLs with parameters as labels.
- `GO-12.12` [SHOULD] expose `net/http/pprof` on a separate, internal-only listener, including the `goroutineleak` profile.
- `GO-12.13` [MUST] expose `/healthz` (process is alive) and `/readyz` (dependencies are reachable) on services.

## 13. Configuration, CLIs and startup

### Configuration

- `GO-13.1` [MUST] load configuration once in `main` into a typed `Config` struct and pass the parts each component needs. [MUST NOT] call `os.Getenv` deep in the code.
- `GO-13.2` [MUST] validate configuration at startup and exit with a clear message listing every problem (use `errors.Join`).
- `GO-13.3` [SHOULD] read configuration from environment variables (twelve-factor) with an optional file for local development; precedence is flags > environment > file > defaults.
- `GO-13.4` [MUST] give every setting a safe default or make it required; document both in a `Config` doc comment or `config.example.toml`.
- `GO-13.5` [MUST NOT] commit secrets. Read them from the environment, a mounted file or a secret manager, and keep them in typed values that redact themselves in logs.
- `GO-13.6` [SHOULD] use `time.Duration` and typed sizes in config (`30s`, `512MiB`), not bare integers with units in the name.

### CLIs

- `GO-13.7` [MUST] use the standard `flag` package for small tools, and `spf13/cobra` (or `urfave/cli`) only when there are subcommands.
- `GO-13.8` [MUST] print usage to stderr, results to stdout, and exit with `0` on success, `1` on failure and `2` on bad usage.
- `GO-13.9` [SHOULD] support `--json` output for any command whose output another tool may read.
- `GO-13.10` [MUST] include a `version` command or `--version` flag that prints the version, commit and build date injected at build time.

### main and run

- `GO-13.11` [MUST] keep `main` tiny: it calls `run`, prints the error and sets the exit code. `run` takes its inputs (args, env, stdout) as parameters so it is testable.

```go
func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	if err := run(ctx, os.Args[1:], os.Getenv, os.Stdout); err != nil {
		fmt.Fprintf(os.Stderr, "projectd: %v\n", err)
		os.Exit(1)
	}
}

func run(ctx context.Context, args []string, getenv func(string) string, stdout io.Writer) error {
	cfg, err := config.Load(args, getenv)
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}

	log := platform.NewLogger(stdout, cfg.Log)

	db, err := postgres.Open(ctx, cfg.Database)
	if err != nil {
		return fmt.Errorf("open database: %w", err)
	}
	defer db.Close()

	svc := invoice.NewService(postgres.NewInvoiceStore(db), log)
	srv := httpapi.NewServer(cfg.HTTP, svc, log)

	return platform.Serve(ctx, srv, log)
}
```

## 14. HTTP services

### Routing and handlers

- `GO-14.1` [MUST] route with the standard `http.ServeMux` patterns (method + path + wildcards: `"GET /invoices/{id}"`) and read wildcards with `r.PathValue("id")`. [MUST NOT] add a router dependency for routing alone.
- `GO-14.2` [MUST] register all routes in one `routes()` function per server so the whole API is visible in one place.
- `GO-14.3` [MUST] keep handlers thin: decode and validate input, call one service method, encode the response. Business rules live in the domain package.
- `GO-14.4` [SHOULD] write handlers as methods on a server struct that holds dependencies, or as functions returning `http.Handler` that close over them. [MUST NOT] reach for globals.
- `GO-14.5` [SHOULD] use a small adapter so handlers return an error and a single place maps errors to responses.

### Middleware

- `GO-14.6` [MUST] write middleware as `func(http.Handler) http.Handler` and compose it in `routes()` in a visible order: recover → request ID → logging → metrics → auth → handler.
- `GO-14.7` [MUST] protect browser-facing state-changing routes with `http.CrossOriginProtection` (Go 1.25+) or an equivalent CSRF defence.

### Requests and responses

- `GO-14.8` [MUST] bound request bodies with `http.MaxBytesReader` and reject unknown JSON fields on write endpoints.
- `GO-14.9` [SHOULD] use `encoding/json/v2` (Go 1.27) in new code: it rejects invalid UTF-8 and duplicate keys by default and unmarshals much faster. Existing `encoding/json` code keeps working and is now backed by v2.
- `GO-14.10` [MUST] return errors as RFC 9457 problem details (`application/problem+json`) with the right status code; never leak internal error text on 5xx.
- `GO-14.11` [MUST] set `Content-Type` on every response and write the status before the body.

```go
func (s *Server) routes() http.Handler {
	mux := http.NewServeMux()
	mux.Handle("GET /invoices/{id}", s.handle(s.getInvoice))
	mux.Handle("POST /invoices/{id}/pay", s.handle(s.payInvoice))
	mux.HandleFunc("GET /healthz", healthz)

	var h http.Handler = mux
	h = s.auth(h)
	h = s.metrics(h)
	h = s.logRequests(h)
	h = requestID(h)
	h = recoverPanics(h, s.log)
	return h
}

func (s *Server) getInvoice(w http.ResponseWriter, r *http.Request) error {
	id, err := invoice.ParseID(r.PathValue("id"))
	if err != nil {
		return badRequest("id must be a UUID")
	}
	inv, err := s.invoices.Get(r.Context(), id)
	if err != nil {
		return err // mapped to 404/500 in s.handle
	}
	return writeJSON(w, http.StatusOK, toInvoiceResponse(inv))
}
```

### Servers and clients

- `GO-14.12` [MUST] set `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout` and `IdleTimeout` on every `http.Server`. [MUST NOT] use `http.ListenAndServe` with the default server in production.
- `GO-14.13` [MUST] shut down gracefully: on context cancellation call `srv.Shutdown` with a deadline, then close dependencies.
- `GO-14.14` [MUST] reuse one `http.Client` per dependency with `Timeout` set. [MUST NOT] use `http.DefaultClient` or `http.Get` for outbound calls.
- `GO-14.15` [MUST] close every response body (`defer resp.Body.Close()`), and check the status code before decoding.
- `GO-14.16` [SHOULD] retry only idempotent requests, with exponential backoff and jitter, honouring `Retry-After`.

## 15. Data access

- `GO-15.1` [MUST] use PostgreSQL through `jackc/pgx/v5` (`pgxpool`) for new services; `database/sql` is fine for other databases or libraries that must stay driver-neutral.
- `GO-15.2` [SHOULD] write SQL by hand and generate type-safe Go with **sqlc**. [AVOID] ORMs; a query builder (`squirrel`) is allowed only for genuinely dynamic filters.
- `GO-15.3` [MUST] pass every value as a bind parameter (`$1`). [MUST NOT] build SQL with `fmt.Sprintf` or string concatenation on input.
- `GO-15.4` [MUST] pass `ctx` to every query so cancellation and deadlines reach the database.
- `GO-15.5` [MUST] keep SQL types out of the domain: the `postgres` package maps rows to `invoice.Invoice` and storage errors to domain errors.
- `GO-15.6` [MUST] run multi-statement writes in a transaction through one helper that commits on success, rolls back on error or panic, and retries serialization failures.
- `GO-15.7` [MUST] use UUIDv7 primary keys generated in Go (or with `uuidv7()` in PostgreSQL 18), stored as the native `uuid` type.
- `GO-15.8` [MUST] close `rows` and check `rows.Err()`; prefer pgx's `pgx.CollectRows` helpers, which do both.
- `GO-15.9` [MUST] configure pool limits (`MaxConns`, `MaxConnLifetime`, `MaxConnIdleTime`) explicitly.
- `GO-15.10` [MUST] manage schema with numbered SQL migrations (`goose` or `golang-migrate`) committed in `migrations/`; each runs in a transaction and is never edited after it ships.
- `GO-15.11` [SHOULD] test stores against a real PostgreSQL started by `testcontainers-go`, not mocks.

```go
func (s *Store) WithTx(ctx context.Context, fn func(q *db.Queries) error) (err error) {
	tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return fmt.Errorf("begin: %w", err)
	}
	defer func() {
		if err != nil {
			err = errors.Join(err, tx.Rollback(ctx))
		}
	}()

	if err := fn(s.queries.WithTx(tx)); err != nil {
		return err
	}

	return tx.Commit(ctx)
}
```

## 16. Agents and daemons

Many Go programs here are long-running agents: they sit on a host, poll hardware or the network, and report to a control plane. They run unattended for months, so they get extra rules.

### Lifecycle

- `GO-16.1` [MUST] run every component as `Run(ctx) error` that returns when `ctx` is cancelled, and start them together with an `errgroup` so one failure stops the rest cleanly.
- `GO-16.2` [MUST] handle `SIGTERM` and `SIGINT` by cancelling the root context, then give in-flight work a bounded grace period (for example 15 seconds) before exiting.
- `GO-16.3` [SHOULD] reload configuration on `SIGHUP` without restarting, when the agent supports it.
- `GO-16.4` [MUST] run under a supervisor (systemd with `Restart=on-failure`, a container runtime, or a Windows service) and [MUST NOT] daemonize itself.
- `GO-16.5` [SHOULD] notify systemd of readiness and watchdog pings (`sd_notify`) when deployed with `Type=notify`.

### Loops, retries and the control plane

- `GO-16.6` [MUST] drive periodic work with `time.Ticker` (stopped with `defer t.Stop()`) inside a `select` on `ctx.Done()`.
- `GO-16.7` [MUST] retry failed connections with capped exponential backoff and jitter; [MUST NOT] spin in a tight retry loop.
- `GO-16.8` [MUST] buffer reports locally (bounded queue or on-disk spool) while the control plane is unreachable, and drop the oldest data with a counter when the buffer fills.
- `GO-16.9` [MUST] authenticate to the control plane with mTLS or a per-agent token, and pin the control plane's CA.
- `GO-16.10` [MUST] keep resource use bounded and visible: cap goroutines, buffers and file handles, and report memory, goroutine count and queue depth as metrics.

### Updates and identity

- `GO-16.11` [MUST] verify the signature (or checksum from a signed manifest) of any self-update before swapping binaries, and keep the previous binary for rollback.
- `GO-16.12` [MUST] generate a stable agent ID (UUIDv7) on first start and persist it; never derive identity from hostname or IP alone.
- `GO-16.13` [SHOULD] report version, commit, OS, architecture and uptime with every heartbeat.

```go
func (a *Agent) Run(ctx context.Context) error {
	ticker := time.NewTicker(a.interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return a.flush(context.WithoutCancel(ctx))
		case <-ticker.C:
			if err := a.collectAndReport(ctx); err != nil {
				a.log.WarnContext(ctx, "report failed", "err", err, "queued", a.queue.Len())
			}
		}
	}
}
```

## 17. Performance and memory

- `GO-17.1` [MUST] measure before optimizing: a benchmark (`b.Loop()`) or a profile (`pprof` CPU, heap, mutex, block) must show the problem, and the pull request includes before and after numbers from `benchstat`.
- `GO-17.2` [SHOULD] build release binaries with profile-guided optimization (`default.pgo` from a production CPU profile) for hot services.
- `GO-17.3` [SHOULD] preallocate slices and maps when the size is known: `make([]Line, 0, len(rows))`.
- `GO-17.4` [SHOULD] use `strings.Builder` / `bytes.Buffer` for building strings in loops, and `strconv` over `fmt.Sprintf` on hot paths.
- `GO-17.5` [SHOULD] stream large payloads (`io.Reader`, `json.NewDecoder`, iterators) instead of loading them fully into memory.
- `GO-17.6` [MAY] use `sync.Pool` for frequently allocated, short-lived buffers; [MUST] reset objects before returning them and never pool objects that hold references to request data.
- `GO-17.7` [SHOULD] set `GOMEMLIMIT` in containers (around 90% of the memory limit) and leave `GOGC` at its default unless a profile says otherwise. Go 1.26+'s Green Tea collector already lowers GC cost.
- `GO-17.8` [MUST NOT] use `unsafe` or `reflect` for speed without a benchmark, a comment explaining the invariant, and a test that guards it.
- `GO-17.9` [AVOID] premature concurrency: a goroutine per item is often slower than a simple loop for small, CPU-cheap work.
- `GO-17.10` [SHOULD] keep struct fields ordered for readability first; reorder for alignment only in types allocated millions of times, with a comment.

## 18. Security

- `GO-18.1` [MUST] run `govulncheck` in CI and fix reachable vulnerabilities before release; keep dependencies current with Renovate or Dependabot.
- `GO-18.2` [MUST] validate all input at the boundary (lengths, ranges, formats, enums) before it reaches the domain.
- `GO-18.3` [MUST] use bind parameters for SQL (§15) and `exec.CommandContext` with an argument list — never a shell string — for subprocesses.
- `GO-18.4` [MUST] open user-influenced paths through `os.Root` (Go 1.24+) or `filepath.IsLocal` checks to prevent path traversal.
- `GO-18.5` [MUST] use `crypto/rand` for tokens, keys and IDs; `math/rand/v2` is for simulations and jitter only.
- `GO-18.6` [MUST] compare secrets with `crypto/subtle.ConstantTimeCompare` or `hmac.Equal`.
- `GO-18.7` [MUST] hash passwords with `argon2id` or `bcrypt` (`golang.org/x/crypto`), never with a fast hash.
- `GO-18.8` [MUST] use TLS 1.3 by default with the standard library's defaults (post-quantum hybrid key exchange is on by default since Go 1.26). [MUST NOT] set `InsecureSkipVerify` outside tests.
- `GO-18.9` [MUST] use mTLS for service-to-service and agent-to-control-plane traffic on untrusted networks.
- `GO-18.10` [MUST] set HTTP security headers on browser-facing services and protect state-changing routes from cross-origin requests (§14).
- `GO-18.11` [MUST NOT] log or return secrets, stack traces or internal errors to clients.
- `GO-18.12` [SHOULD] run services as a non-root user in a minimal image (`distroless/static` or `scratch`) with a read-only root filesystem.
- `GO-18.13` [SHOULD] enable FIPS 140-3 mode (`GOFIPS140`) only when a customer requires it, and test the build with it.

## 19. Testing

### Structure

- `GO-19.1` [MUST] keep tests beside the code in `<name>_test.go`, and shared fixtures in `testdata/`.
- `GO-19.2` [MUST] use the standard `testing` package. [MAY] use `go-cmp` (`cmp.Diff`) for comparing structs; [AVOID] assertion frameworks that hide the failing value.
- `GO-19.3` [MUST] write table-driven tests with named subtests (`t.Run(tc.name, ...)`) when a function has more than two interesting cases.
- `GO-19.4` [MUST] write failure messages as `Func(input) = got, want want`: `t.Errorf("Parse(%q) = %v, want %v", in, got, want)`.
- `GO-19.5` [MUST] call `t.Helper()` in test helpers, use `t.Cleanup` for teardown, `t.TempDir()` for files and `t.Context()` (Go 1.24+) for contexts.
- `GO-19.6` [SHOULD] mark independent tests `t.Parallel()`.
- `GO-19.7` [MUST] run the suite with `go test -race -shuffle=on ./...` in CI.

### Time, concurrency and I/O

- `GO-19.8` [MUST] test time-dependent and concurrent code with `testing/synctest`: inside `synctest.Test`, time is virtual and `synctest.Wait` / `synctest.Sleep` (Go 1.27) make goroutine scheduling deterministic. [MUST NOT] use real `time.Sleep` in tests.
- `GO-19.9` [SHOULD] use `httptest.NewServer` (or `httptest.NewTestServer` with synctest, Go 1.27) for HTTP clients, and `httptest.NewRecorder` for handlers.
- `GO-19.10` [SHOULD] test stores against real PostgreSQL via `testcontainers-go`, gated by a build tag or `-short` so unit runs stay fast.
- `GO-19.11` [SHOULD] prefer hand-written fakes (an in-memory `Store`) over generated mocks.

### Fuzzing, benchmarks and golden files

- `GO-19.12` [SHOULD] add fuzz tests (`func FuzzParse(f *testing.F)`) for parsers, decoders and anything that reads untrusted input, and commit the crashers they find to `testdata/fuzz`.
- `GO-19.13` [SHOULD] write benchmarks with `for b.Loop() { ... }` and compare runs with `benchstat`.
- `GO-19.14` [MAY] use golden files for large outputs, regenerated with a `-update` flag; save debug output in `t.ArtifactDir()` (Go 1.26).

```go
func TestParseAmount(t *testing.T) {
	tests := []struct {
		name    string
		in      string
		want    Cents
		wantErr bool
	}{
		{name: "whole", in: "12", want: 1200},
		{name: "cents", in: "12.34", want: 1234},
		{name: "negative", in: "-1", wantErr: true},
		{name: "empty", in: "", wantErr: true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got, err := ParseAmount(tc.in)
			if (err != nil) != tc.wantErr {
				t.Fatalf("ParseAmount(%q) error = %v, wantErr %v", tc.in, err, tc.wantErr)
			}
			if got != tc.want {
				t.Errorf("ParseAmount(%q) = %d, want %d", tc.in, got, tc.want)
			}
		})
	}
}

func TestAgentReportsEveryInterval(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		sink := &fakeSink{}
		a := NewAgent(sink, 30*time.Second)
		ctx, cancel := context.WithCancel(t.Context())
		go a.Run(ctx)

		synctest.Sleep(95 * time.Second)
		if got := sink.Count(); got != 3 {
			t.Errorf("reports after 95s = %d, want 3", got)
		}
		cancel()
		synctest.Wait()
	})
}
```

## 20. Documentation and comments

- `GO-20.1` [MUST] give every package a package comment (`// Package invoice ...`) in one file, usually `doc.go` for larger packages.
- `GO-20.2` [MUST] document every exported identifier with a full sentence that starts with its name: `// NewService returns a Service backed by store.`
- `GO-20.3` [SHOULD] document what is not obvious from the signature: concurrency safety, ownership of returned slices, units, zero-value behaviour and the errors a caller can expect.
- `GO-20.4` [MUST] write comments that explain **why**, not what the next line does.
- `GO-20.5` [MUST] use the doc-comment syntax `go doc` renders: `[Service.MarkPaid]` links, `# Headings`, indented code blocks and lists.
- `GO-20.6` [SHOULD] add runnable `Example` functions for public APIs; they are compiled and checked by `go test`.
- `GO-20.7` [MUST] format TODOs as `// TODO(owner): what and why`, with an issue link for anything longer than a day.
- `GO-20.8` [MUST] mark deprecated APIs with a `// Deprecated: use X instead.` paragraph.
- `GO-20.9` [MUST] keep a `README.md` per repository with: what it is, how to run it locally (one command), how to test, configuration and how it is deployed.
- `GO-20.10` [SHOULD] record significant design decisions as short ADRs in `docs/adr/`.

## 21. Git, commits, releases and review

### Commits

- `GO-21.1` [MUST] follow **Conventional Commits**: `type(scope): summary` in the imperative, lower case, no trailing period, under 72 characters.
- Types: `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `build`, `ci`, `chore`, `style`, `revert`. Scope is the package or area: `fix(agent): back off when the control plane is down`.
- `GO-21.2` [MUST] mark breaking changes with `!` and a `BREAKING CHANGE:` footer.
- `GO-21.3` [SHOULD] explain the *why* in the body when it is not obvious from the summary.
- `GO-21.4` [MUST] keep every commit building and passing tests; squash fix-ups before merging.

### Branches and pull requests

- `GO-21.5` [MUST] branch from `main` with a short typed name: `feat/agent-spool`, `fix/invoice-rounding`.
- `GO-21.6` [SHOULD] keep pull requests under about 400 changed lines, excluding generated code.
- `GO-21.7` [MUST] pass `task lint` and `task test` before review, and keep `go.mod` tidy (`go mod tidy` produces no diff).
- `GO-21.8` [MUST] include tests and migrations in the same pull request as the change they support.

### Releases

- `GO-21.9` [MUST] tag releases with semantic versions (`v1.4.0`) and generate the changelog from Conventional Commits.
- `GO-21.10` [MUST] inject version information at build time: `-ldflags "-X example.com/project/internal/version.Version={{.Version}} -X ...Commit={{.Commit}}"`, and fall back to `debug.ReadBuildInfo()` for `go install` builds.
- `GO-21.11` [SHOULD] build with GoReleaser: reproducible (`-trimpath`, `CGO_ENABLED=0`), multi-platform, checksummed and signed, with container images and an SBOM.

### Review checklist

- Is every error checked, wrapped with context and handled once?
- Does every blocking call take a context with a deadline?
- Does every goroutine have a clear owner and exit path, and is it waited for?
- Are interfaces small and defined by the consumer?
- Is the exported surface as small as it can be, and documented?
- Are inputs validated at the boundary and SQL parameterised?
- Are there table tests for the new behaviour, run with `-race`?

## Appendix

### A. `.golangci.yml`

```yaml
version: "2"

run:
  timeout: 5m

linters:
  default: standard
  enable:
    - bodyclose
    - contextcheck
    - copyloopvar
    - errorlint
    - exhaustive
    - gocritic
    - gosec
    - misspell
    - nilerr
    - noctx
    - nolintlint
    - perfsprint
    - revive
    - rowserrcheck
    - sloglint
    - sqlclosecheck
    - unconvert
    - unparam
    - usestdlibvars
    - wrapcheck
  settings:
    nolintlint:
      require-explanation: true
      require-specific: true
    sloglint:
      kv-only: true
      context: scope
      key-naming-case: snake
    exhaustive:
      default-signifies-exhaustive: true

formatters:
  enable:
    - gofumpt
    - goimports
  settings:
    goimports:
      local-prefixes:
        - example.com/project
```

### B. `Taskfile.yml`

```yaml
version: "3"

tasks:
  fmt:
    cmds:
      - go tool golangci-lint fmt
  lint:
    cmds:
      - go vet ./...
      - go tool golangci-lint run
      - go tool govulncheck ./...
  test:
    cmds:
      - go test -race -shuffle=on -count=1 ./...
  test:integration:
    cmds:
      - go test -race -tags=integration ./...
  generate:
    cmds:
      - go generate ./...
      - go tool sqlc generate
  modernize:
    cmds:
      - go fix ./...
  build:
    cmds:
      - CGO_ENABLED=0 go build -trimpath -o bin/ ./cmd/...
```

### C. `go.mod` header

```text
module example.com/project

go 1.27.0

toolchain go1.27.0

tool (
    github.com/golangci/golangci-lint/v2/cmd/golangci-lint
    golang.org/x/tools/cmd/stringer
    golang.org/x/vuln/cmd/govulncheck
    github.com/sqlc-dev/sqlc/cmd/sqlc
)
```

### D. New package checklist

- [ ] Name is short, lower case, says what it provides, and does not stutter at call sites.
- [ ] Package comment and doc comments on every exported identifier.
- [ ] Dependencies passed in through a constructor; no globals or `init()` I/O.
- [ ] Interfaces it needs are declared here and kept small.
- [ ] Every I/O function takes `ctx` first and returns `error` last.
- [ ] Errors wrapped with context; sentinel or typed errors for conditions callers branch on.
- [ ] Tests beside the code, table-driven, passing with `-race`.

### E. Sources

- [Go 1.27 release notes](https://go.dev/doc/go1.27)
- [Go 1.26 release notes](https://go.dev/doc/go1.26)
- [Effective Go](https://go.dev/doc/effective_go)
- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
- [Google Go Style Guide](https://google.github.io/styleguide/go/)
