Claude Skill

sota-golang

State-of-the-art Go engineering rules (2026 baseline, Go 1.25+) that Claude applies when writing new Go code or auditing existing Go code. Covers error handling, interface/package design, goroutine and channel correctness, net/http hardening, security (SQL, exec, path traversal,

LLM Mart · 0 points · 9 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download martinholovsky-SOTA-skills-skills_sota-golang-ec2abf6.zip · 46 KB
Part of martinholovsky/sota-skills — 39 skills

Install

skills CLI npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-golang
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
Git git clone https://github.com/martinholovsky/SOTA-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SOTA Go (2026)

Expert-level rules for producing and auditing production Go. Baseline language version: Go 1.25+, the oldest release still in security support (Go fixes the last two majors; 1.24 left support with 1.26's release, 2026-02). Feature notes: loop-var scoping from 1.22, b.Loop/os.Root/tool directives from 1.24, testing/synctest and container-aware GOMAXPROCS from 1.25, errors.AsType and the default-on Green Tea GC from 1.26 — noted where relevant. Every rule states the why; every rules file ends with an audit checklist of grep/vet/lint patterns.

Purpose

Two consumers, one source of truth:

  • BUILD mode — generating new Go code: follow the rules as defaults, not suggestions. Deviate only with an explicit comment justifying it.
  • AUDIT mode — reviewing existing Go code: hunt violations using the audit checklists, classify by severity, report in the finding format below.

BUILD mode

  1. Before writing code, read the rules files relevant to the task (see index). A service touching HTTP + DB + goroutines needs 03, 04, 05.
  2. Apply the top-10 non-negotiables (below) unconditionally.
  3. New modules: go mod init with a real module path; since 1.26 it writes the previous minor as the go directive (e.g. go 1.25.0) for ecosystem compatibility — keep that unless you need newer language features; pin the toolchain directive to the current patch release. Add golangci-lint config and a CI step running go vet, golangci-lint run, go test -race ./..., govulncheck ./... from day one (see rules/07).
  4. Prefer stdlib. Each dependency must earn its place (see rules/05 supply chain section).
  5. Write table tests alongside the code, not after. Exported behavior gets a test; concurrency gets a -race test; parsers get a fuzz target.
  6. When generating code that violates a rule for a legitimate reason (e.g. sync.Pool complexity, unsafe), leave a // NOTE(sota): comment explaining the trade-off so auditors don't flag it blind.

AUDIT mode

Work through each relevant rules file's audit checklist against the target repo. Run the listed grep/vet/lint commands; confirm each hit manually before reporting (greps are recall-oriented, expect false positives).

Severity conventions

Severity Meaning Examples
CRITICAL Exploitable or guaranteed-incorrect in production SQL built with fmt.Sprintf, command injection via sh -c, unbounded goroutine leak on hot path, InsecureSkipVerify: true, data race confirmed by -race
HIGH Likely production incident or security weakness Missing http.Server timeouts, no ctx cancellation on blocking goroutine, unchecked integer truncation on attacker input (G115), resp.Body never closed, panic for control flow in a server
MEDIUM Correctness/maintainability hazard, latent bug Error strings compared with strings.Contains, context stored in struct, time.After in a loop, map writes without lock under suspected concurrency, missing errors.Is/As
LOW Idiom/perf debt, works but wrong shape Returning interfaces, util package dumps, missing preallocation on hot path, non-table tests, no t.Parallel
INFO Style, doc, or hygiene note Naming, missing doc comments, gofumpt drift

Finding format

[SEVERITY] file.go:LINE — short title
  Rule: rules/NN-name.md § section
  Evidence: the offending line(s), verbatim
  Impact: one sentence — what goes wrong, under what conditions
  Fix: concrete replacement code or action
  Effort: trivial | small | medium | large

Group findings by severity, CRITICAL first. End the audit with: counts per severity, the three highest-leverage fixes, and which checklists were run.

Rules index

File Read this when...
rules/01-errors.md Writing/reviewing any error path: wrapping with %w, errors.Is/As, sentinel vs typed errors, in-band sentinels (absence encoded as -1/0/"") and comma-ok, panic/recover policy, error API design for libraries vs apps
rules/02-design.md Designing packages or APIs: interface placement and size, package layout and internal/, naming, zero values, generics restraint, embedding, functional options, context.Context discipline
rules/03-concurrency.md Anything with go, chan, sync, or select: goroutine lifecycle ownership, leak catalog, errgroup fan-out, channels-vs-mutex decision, race patterns, worker pools, semaphores, time.After traps
rules/04-http-services.md Building or auditing HTTP servers/clients: all five server timeouts, client timeouts and body hygiene, connection reuse, graceful shutdown, middleware, slog structured logging, request-scoped values
rules/05-security.md Any input crossing a trust boundary: SQL parameterization, os/exec safety, path traversal and os.Root, integer overflow (G115), output encoding (html/template), CSPRNG (crypto/rand vs math/rand), TLS config, unsafe/cgo policy, govulncheck, supply chain and go.sum
rules/06-performance.md Latency/memory work: pprof workflow, testing.B + b.Loop, allocation reduction, strings.Builder, sync.Pool criteria, escape analysis, GOGC/GOMEMLIMIT, PGO
rules/07-tooling-ci.md Setting up or auditing CI and tests: golangci-lint curated config, staticcheck/gofumpt/vet, table tests, t.Parallel correctness, testcontainers, golden files, fuzzing, go.mod hygiene and tool directives. Test strategy — suite shape, TDD, doubles, test data, flake policy — lives in sota-testing; load it for any build that writes logic. This file owns Go runner mechanics only.

Top-10 non-negotiables

  1. Every error is handled or wrapped with %w and context — never discarded with _, never logged-and-ignored on a path that must abort. Compare with errors.Is/errors.As, never string matching. (rules/01)
  2. No panics for control flow. panic is for unreachable programmer errors only; servers recover at goroutine boundaries and log. (rules/01)
  3. Every goroutine has an owner and a guaranteed exit path — tied to a context.Context, a closed channel, or a WaitGroup/errgroup join. If you can't say how it stops, don't start it. (rules/03)
  4. go test -race ./... in CI, always. A race detector failure is a CRITICAL finding, not flaky-test noise. (rules/03, rules/07)
  5. http.Server sets ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout; clients set timeouts and defer resp.Body.Close() with drain. Default zero timeouts are a DoS. (rules/04)
  6. SQL only via parameterized queries (database/sql placeholders, pgx, or sqlc-generated code). String-built SQL is CRITICAL, no exceptions for "internal" values. (rules/05)
  7. os/exec with argv lists, never sh -c with interpolated input; file paths validated against a root (os.Root on 1.24+, else filepath.Clean + prefix check after resolving symlinks). (rules/05)
  8. context.Context is the first parameter, flows down, is never stored in a struct, and carries only request-scoped metadata — never dependencies. (rules/02)
  9. Accept interfaces, return structs; define interfaces at the consumer, keep them small. No premature interfaces "for mocking". (rules/02)
  10. govulncheck ./... and golangci-lint gate CI; go.sum committed; dependencies minimal and justified. (rules/05, rules/07)
Files (sota-skills)
  • rules
    • 01-errors.md 12.8 KB
      # 01 — Errors: handling, wrapping, design
      
      Errors are values. They are part of your API surface and your observability
      story. Most production incidents in Go services trace back to an error that
      was dropped, double-logged, string-matched, or panicked on.
      
      ## 1. Handle every error, exactly once
      
      **Handle = one of:** return it (usually wrapped), act on it (retry, fallback,
      default), or — only at the top of a call stack — log it and continue/abort.
      Doing two of these for the same error is a bug: log-and-return produces
      duplicate log lines with no extra information.
      
      ```go
      // BAD — double handling: caller will log it again
      if err != nil {
          log.Printf("query failed: %v", err)
          return err
      }
      
      // GOOD — add context, return once; the top-level handler logs
      if err != nil {
          return fmt.Errorf("query user %d: %w", id, err)
      }
      ```
      
      Never discard with `_` unless the API genuinely cannot fail in context and you
      say why:
      
      ```go
      // BAD
      b, _ := json.Marshal(resp)
      
      // GOOD — justified discard, documented
      // Marshal of a struct with no chan/func/cycles cannot fail.
      b, _ := json.Marshal(resp) //nolint:errcheck // statically infallible
      ```
      
      In practice prefer handling anyway; `errcheck` (via golangci-lint) enforces
      this and exceptions should be rare and annotated.
      
      `defer f.Close()` on **writes** loses the error that tells you the write
      failed (buffered data flushes on close). Capture it:
      
      ```go
      // GOOD — close error matters for writers
      defer func() {
          if cerr := w.Close(); cerr != nil && err == nil {
              err = fmt.Errorf("close %s: %w", path, cerr)
          }
      }()
      ```
      
      For read-only files, `defer f.Close()` discarding the error is acceptable.
      
      ## 2. Wrap with %w; add context, not noise
      
      `fmt.Errorf("...: %w", err)` preserves the chain for `errors.Is/As`. Use `%v`
      only when you deliberately want to *break* the chain (hiding an internal error
      from callers — a real, intentional choice at package boundaries).
      
      Context rules:
      
      - Say what *you* were doing, with identifiers: `"load config %q: %w"`. Do not
        restate what the callee already says (`"failed to open file: open file..."`).
      - No `"failed to"` / `"error:"` prefixes — chains read as
        `"a: b: c: underlying"`. Lowercase, no trailing punctuation
        (staticcheck ST1005).
      - Wrap at each layer that adds information; pass through (`return err`)
        when you have nothing to add. Mechanical wrapping at every return is noise.
      
      ```go
      // BAD — no chain (%v), redundant phrasing, capitalized
      return fmt.Errorf("Failed to read the file: %v", err)
      
      // GOOD
      return fmt.Errorf("read manifest %s: %w", path, err)
      ```
      
      Multiple causes: `errors.Join(errA, errB)` (Go 1.20+) or
      `fmt.Errorf("...: %w; also: %w", e1, e2)`. `errors.Is/As` traverse joined
      trees. Typical use: accumulating cleanup errors, validating many fields.
      
      ## 3. Inspect with errors.Is / errors.As — never strings
      
      ```go
      // BAD — breaks on wrapping, wording changes, localization
      if strings.Contains(err.Error(), "not found") { ... }
      if err == sql.ErrNoRows { ... } // breaks once anything wraps it
      
      // GOOD
      if errors.Is(err, sql.ErrNoRows) { ... }
      
      var pgErr *pgconn.PgError
      if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation { ... }
      
      // GOOD — Go 1.26+: errors.AsType[E error](err) (E, bool) is the type-safe,
      // faster replacement for errors.As; prefer it on 1.26+ codebases
      if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok &&
          pgErr.Code == pgerrcode.UniqueViolation { ... }
      ```
      
      `==` on errors is only valid where wrapping is impossible (comparing to `nil`,
      or inside the package that just created the sentinel). Audit any `err ==`
      against exported sentinels as MEDIUM.
      
      Context errors: check `errors.Is(err, context.Canceled)` /
      `context.DeadlineExceeded` before counting a failure as a real one — a
      canceled request is not a dependency outage; don't page on it.
      
      ### 4a. In-band sentinels — Go's are documented, yours are not
      
      Go's stdlib deliberately returns in-band sentinels and **documents them**:
      `strings.Index`/`LastIndex` return `-1` when not found (verified, go1.26.5). That is
      idiomatic and fine *because the contract is published and every caller is expected
      to test it*. The defect is the undocumented one you write yourself — the class is
      `sota-architecture` rules/02 §8a.
      
      - `strconv.Atoi("x")` returns `(0, err)` (verified, go1.26.5). The `0` is a perfectly
        ordinary value; the error is the only thing distinguishing it from `Atoi("0")`.
        Dropping `err` converts an out-of-band signal into an in-band one, which is
        precisely what `errcheck` exists to stop.
      - Go has no sum type, so the out-of-band alternatives are **comma-ok**
        (`v, ok := m[k]`) or `(T, error)`. Return one of them; a `func f() int` that
        answers `-1` for "no value" has no way to say why.
      - `*int` for "optional int" is legal and usually worse than comma-ok — it moves the
        check to a nil dereference and heap-allocates. Prefer `(int, bool)`.
      - Audit: `grep -rnE 'return -1|return 0, nil' --include='*.go' .` for producers, then
        the tell that actually finds the bug — a comparison where one side is filtered
        against the sentinel and the other is not.
      
      ## 4. Sentinel vs typed vs opaque — choosing the error kind
      
      | Kind | When | Example |
      |---|---|---|
      | **Opaque** (just `error`) | Caller can only propagate/log. Default. | most internal funcs |
      | **Sentinel** (`var ErrX = errors.New`) | Caller branches on *which* condition, no payload needed | `io.EOF`, `sql.ErrNoRows`, `fs.ErrNotExist` |
      | **Typed** (struct implementing `error`) | Caller needs structured data from the failure | `*fs.PathError`, validation errors with field names |
      
      Rules:
      
      - Every exported sentinel/type is **API forever**. Export the minimum; start
        opaque, promote to sentinel/typed when a real caller needs to branch.
      - Sentinels: `Err` prefix, `var ErrQuotaExceeded = errors.New("quota exceeded")`.
        Wrap them when returning: `fmt.Errorf("user %d: %w", id, ErrQuotaExceeded)`.
      - Typed errors: name ends in `Error`, pointer receiver, and **return the
        concrete pointer only into an `error` variable immediately** — a typed nil
        pointer stored in an `error` interface is non-nil:
      
      ```go
      // BAD — classic typed-nil bug: returns non-nil error interface
      func do() *QueryError { ... return nil }
      var err error = do() // err != nil even though pointer is nil!
      
      // GOOD — functions return `error`, not concrete error types
      func do() error {
          if bad {
              return &QueryError{Query: q, Err: cause}
          }
          return nil
      }
      ```
      
      - Implement `Unwrap() error` (or `Unwrap() []error`) on wrapper types so
        `errors.Is/As` see through them.
      - Libraries: prefer behavior interfaces over exported types when feasible
        (`interface{ Timeout() bool }` à la `net.Error`), so callers depend on
        capability, not identity.
      
      ## 5. No panic for control flow
      
      `panic` means "programmer bug, state is corrupt, crashing is correct":
      impossible switch cases, broken invariants, failed init of must-exist state.
      It is never a substitute for returning an error on input, I/O, network, parse,
      or not-found conditions.
      
      ```go
      // BAD — user input panics the server
      func ParseLevel(s string) Level {
          l, ok := levels[s]
          if !ok { panic("bad level: " + s) }
          return l
      }
      
      // GOOD
      func ParseLevel(s string) (Level, error) {
          l, ok := levels[s]
          if !ok {
              return 0, fmt.Errorf("unknown level %q", s)
          }
          return l, nil
      }
      ```
      
      Legitimate panic patterns:
      
      - `MustCompile`-style helpers for package-level init with constant input:
        `var pathRe = regexp.MustCompile(...)`. Write your own `MustX` only for
        init-time constants, never runtime data.
      - `panic` across goroutines is fatal: **a panic in a goroutine you spawned
        kills the whole process** regardless of recovers elsewhere. Any goroutine
        running third-party or panic-capable code needs its own deferred recover
        (errgroup does NOT recover for you; `golang.org/x/sync/errgroup` ≥ v0.11
        still propagates panics by re-panicking in `Wait`).
      - HTTP: `net/http` recovers per-request panics by default but the response is
        broken; middleware should recover, log with stack
        (`debug.Stack()`), and return 500. Recover converts to error at the
        boundary; it never resumes business logic.
      
      `recover()` only works in a deferred function in the same goroutine. Audit any
      `recover` outside `defer` as dead code (vet catches some of these).
      
      ## 6. Error flow patterns
      
      - **Guard clauses, happy path left-aligned.** `if err != nil { return ... }`
        immediately; no `else` after a return.
      - **Don't pre-declare `var err error`** and reuse across unrelated calls;
        shadowing bugs (`:=` in an inner scope silently ignoring the outer err)
        are common — `go vet` + golangci-lint `govet` shadow check help.
      - **Errors in loops**: decide explicitly — fail fast (`return` first error),
        or collect (`errors.Join`) and continue. Comment which and why.
      - **errors as part of concurrency**: never send on an error channel without a
        receiver guarantee; prefer `errgroup` which does this correctly
        (see `rules/03`).
      - **Don't log below the top.** Libraries return errors; binaries log them
        once, at the handler/main level, with `slog` and the full chain
        (`slog.Any("err", err)` or `"err", err` — `%+v` style stacks need
        a wrapper lib; stdlib chains carry context strings instead of stacks).
      - **main() pattern**:
      
      ```go
      func main() {
          if err := run(context.Background(), os.Args, os.Stdout); err != nil {
              fmt.Fprintln(os.Stderr, err)
              os.Exit(1)
          }
      }
      ```
      
      `run` returns errors; `main` is the only place that exits. `os.Exit` skips
      defers — never call it (or `log.Fatal`) outside `main`.
      
      ## 7. Library vs application error policy
      
      - **Libraries**: no logging, no `os.Exit`/`log.Fatal`, no leaking
        implementation errors as API (wrap third-party errors with `%v` or your own
        type at the boundary if callers shouldn't depend on them). Document which
        sentinels/types you return.
      - **Applications**: map errors to user/transport meaning in ONE place —
        e.g. an HTTP error mapper translating `ErrNotFound`→404,
        validation type→400, default→500 + log. Scattered status-code decisions
        drift.
      
      ```go
      // GOOD — single translation point
      func httpError(w http.ResponseWriter, err error) {
          switch {
          case errors.Is(err, ErrNotFound):
              http.Error(w, "not found", http.StatusNotFound)
          case errors.As(err, new(*ValidationError)):
              http.Error(w, err.Error(), http.StatusBadRequest)
          default:
              slog.Error("internal error", "err", err)
              http.Error(w, "internal error", http.StatusInternalServerError)
          }
      }
      ```
      
      Never echo internal error chains to external clients (information disclosure —
      see `rules/05`).
      
      ## Audit checklist
      
      Run from repo root; verify each hit manually.
      
      **In-band sentinels — a value standing in for absence** (§4a). Go's own `-1` from
      `strings.Index` is documented and fine when tested immediately; yours is not:
      
      ```bash
      grep -rnE 'return -1$|return -1,' --include='*.go' .        # producer — prefer (T, bool) comma-ok
      grep -rnE ':?= .*strconv\.Atoi\(' --include='*.go' . | grep -v 'err'   # the 0 is in-band if err is dropped
      ```
      
      ```bash
      # Discarded errors (also rely on errcheck via golangci-lint)
      grep -rnE '^\s*[a-zA-Z_].*,\s*_\s*(:?=).*\(' --include='*.go' . | grep -v _test.go
      grep -rn '_ = ' --include='*.go' . | grep -vE '(test|//)'
      
      # String matching on errors — MEDIUM+
      grep -rnE 'strings\.(Contains|HasPrefix|HasSuffix)\(\s*err\.Error\(\)' --include='*.go' .
      grep -rn '\.Error() ==' --include='*.go' .
      
      # == comparison against sentinels (should be errors.Is) — MEDIUM
      grep -rnE 'err\s*[!=]=\s*(sql\.ErrNoRows|io\.EOF|os\.ErrNotExist|context\.(Canceled|DeadlineExceeded))' --include='*.go' .
      
      # %v wrapping where %w likely intended (manual review)
      grep -rnE 'fmt\.Errorf\([^)]*%v[^)]*err\s*\)' --include='*.go' .
      
      # Panics outside main/init/Must/tests — HIGH if reachable from input
      grep -rn 'panic(' --include='*.go' . | grep -vE '(_test\.go|Must|init\()'
      
      # log.Fatal / os.Exit outside main package — HIGH in libraries
      grep -rnE '(log\.Fatal|os\.Exit)' --include='*.go' . | grep -v 'main\.go'
      
      # Error strings: capitalized or "failed to" noise — INFO/LOW (ST1005)
      grep -rnE 'errors\.New\("[A-Z]|fmt\.Errorf\("[A-Z]' --include='*.go' .
      grep -rn 'failed to' --include='*.go' . | grep -E '(errors\.New|fmt\.Errorf)'
      
      # Typed-nil hazard: functions returning concrete error pointer types
      grep -rnE 'func .*\) \*\w+Error( |$)' --include='*.go' .
      
      # recover outside defer; missing Unwrap on wrapper types (manual)
      grep -rn 'recover()' --include='*.go' .
      grep -rln 'type .*Error struct' --include='*.go' . | xargs grep -L 'func (.*) Unwrap()'
      
      # Tooling
      go vet ./...
      golangci-lint run --enable-only errcheck,errorlint,err113,wrapcheck,nilerr ./...
      staticcheck ./...   # ST1005 error strings, SA4006 unused err, SA1019
      ```
      
      Severity guide: string-matched errors MEDIUM (HIGH if driving retry/billing
      logic); dropped error on write/commit path HIGH; panic reachable from external
      input HIGH; double logging LOW; phrasing INFO.
      
    • 02-design.md 12.4 KB
      # 02 — API & package design: interfaces, layout, context, options
      
      Go rewards small, concrete, boring designs. Most design debt in Go codebases
      is premature abstraction: interfaces nobody needed, packages named after
      grab-bags, context misused as a dependency bag.
      
      ## 1. Interfaces: accept interfaces, return structs
      
      - **Define interfaces where they are consumed**, not where implemented. The
        consumer declares the minimal capability it needs; producers just happen to
        satisfy it. This kills import cycles and keeps interfaces honest.
      - **Return concrete types.** Callers get the full method set, godoc, and zero
        indirection; you can add methods without breaking anyone. Returning an
        interface is justified only when multiple implementations are returned from
        the same constructor (e.g. `io.Reader` chosen at runtime) or to hide an
        internal type on purpose.
      - **Keep interfaces small.** 1–2 methods is the sweet spot (`io.Reader`,
        `http.Handler`, `fmt.Stringer`). A 5+ method interface is a sign you're
        modeling an implementation, not a need.
      
      ```go
      // BAD — producer-side, fat, forces every consumer to depend on everything
      package storage
      type Store interface {
          GetUser(ctx context.Context, id int) (*User, error)
          PutUser(ctx context.Context, u *User) error
          ListUsers(ctx context.Context) ([]User, error)
          GetOrder(ctx context.Context, id int) (*Order, error)
          // ... 9 more
      }
      
      // GOOD — consumer-side, minimal
      package billing
      type userGetter interface {
          GetUser(ctx context.Context, id int) (*storage.User, error)
      }
      func NewInvoicer(users userGetter) *Invoicer { ... }
      ```
      
      - **No interfaces "for mocking" by default.** A concrete dependency with a
        consumer-side 1-method interface at the test boundary is enough; you don't
        need a package-wide `XInterface` + `XImpl` pair (a Java-ism; audit as LOW).
      - Compile-time conformance checks where they add safety:
        `var _ http.Handler = (*Server)(nil)`.
      - Don't add methods to satisfy hypothetical future interfaces. Interface
        upgrades via type assertion (`if f, ok := w.(http.Flusher); ok`) are the
        escape hatch when you must sniff optional behavior — document it.
      
      ## 2. Package layout
      
      - **Packages are named after what they provide, not what they contain.**
        `util`, `common`, `helpers`, `misc`, `shared`, `base`, `types` are dumping
        grounds — audit as LOW, refactor target. If a function has no home, it
        belongs next to its single caller, unexported.
      - Package name is part of the call site: `bytes.Buffer`, not
        `bytespkg.BytesBuffer`. No stutter (`http.HTTPServer` → `http.Server`).
        Short, lowercase, no underscores.
      - **`internal/` for anything not meant for external import.** In applications,
        most code goes under `internal/`; `pkg/` is cargo cult — only keep it if the
        repo genuinely exports libraries and the team likes the convention.
      - Typical service shape (don't over-nest; flat beats deep):
      
      ```
      cmd/api/main.go          // wiring only: flags/env, construct deps, call run()
      internal/server/         // HTTP handlers, middleware, routes
      internal/billing/        // domain logic, owns its consumer-side interfaces
      internal/postgres/       // storage impl named after the technology
      go.mod
      ```
      
      - **No package-level mutable state.** Package vars make code untestable,
        order-dependent (`init` graphs), and racy. Dependencies are constructed in
        `main` and passed down (plain constructor injection — no DI framework).
      
      ```go
      // BAD
      var db *sql.DB
      func init() { db = mustOpen() }
      
      // GOOD
      type Server struct{ db *sql.DB }
      func New(db *sql.DB) *Server { return &Server{db: db} }
      ```
      
        Acceptable package-level state: true constants, `regexp.MustCompile` of
        constant patterns, registered `expvar`/metrics where the ecosystem demands
        it. `init()` should be rare and side-effect-light; audit nontrivial `init`
        as MEDIUM.
      - One package per directory; `_test` package suffix (`foo_test`) for
        black-box tests is encouraged for exported-API tests.
      
      ## 3. Naming
      
      - MixedCaps, never snake_case. Acronyms keep case: `ServeHTTP`, `userID`,
        `parseURL` (not `Id`, `Url`).
      - Short names for short scopes (`i`, `r`, `buf`); descriptive names for wide
        scopes and exported identifiers. The wider the scope, the longer the name.
      - Getters drop `Get`: `c.Name()`, not `c.GetName()`. Setters keep `Set`.
      - Constructor: `New` if the package has one main type (`bytes.NewBuffer`...
        actually `buf := bytes.Buffer{}` — prefer usable zero values, §4);
        `NewX` when several.
      - Single-method interface names: method + `er` (`Reader`, `Closer`,
        `Validator`) even when slightly awkward.
      - Receiver names: 1–2 letters, consistent across methods (`s *Server`
        everywhere — never `this`/`self`).
      - Errors: `ErrX` sentinels, `XError` types (see `rules/01`).
      
      ## 4. Zero values as a design tool
      
      Make the zero value useful; it removes constructors, nil checks, and
      initialization ordering bugs.
      
      ```go
      // stdlib exemplars: ready with no constructor
      var buf bytes.Buffer        // usable
      var mu sync.Mutex           // usable
      var wg sync.WaitGroup       // usable
      
      // GOOD — design your types the same way
      type Limiter struct {
          mu    sync.Mutex
          burst int // 0 = unlimited, documented
      }
      ```
      
      - Document zero-value semantics on the type's doc comment.
      - If the zero value is *invalid* (must-have dependencies), force the
        constructor: unexported fields + `NewX`, and make misuse fail fast (nil
        check with a clear panic in the constructor's absence is worse than a
        compile-time-unavoidable constructor).
      - `nil` slices and maps: nil slice reads/appends/ranges fine — don't
        `make([]T, 0)` just to avoid nil (exception: JSON `[]` vs `null` matters).
        Nil **map writes panic** — maps that get written need `make`.
      - Don't return pointers just to enable `nil` as "absent"; prefer
        `(T, bool)` or a zero value with documented meaning.
      
      ## 5. Generics: judicious use only
      
      Generics (1.18+) are for **code that is identical except for the type**:
      containers, slice/map algorithms (`slices`, `maps` packages), constraints on
      ordered/numeric types, type-safe pools.
      
      Do NOT use generics when:
      
      - An interface already expresses it: `func Print(s fmt.Stringer)` beats
        `func Print[T fmt.Stringer](s T)` unless you measured the devirtualization
        win.
      - Only one type instantiation exists in the codebase — YAGNI; write the
        concrete version.
      - The constraint is `any` and you're just avoiding writing two functions —
        readability cost exceeds duplication cost.
      - You're building a generic "repository/service" framework. Domain code stays
        concrete.
      
      Prefer stdlib generics over hand-rolling: `slices.Contains/SortFunc/Index`,
      `maps.Keys`, `cmp.Or` (1.22), `min`/`max` builtins (1.21), `sync.OnceValue`
      (1.21). Audit hand-written loops duplicating these as LOW.
      
      ## 6. Embedding vs composition
      
      - Embedding is **not inheritance**: no overrides seen by the embedded type's
        methods, no LSP. It's automatic delegation only.
      - Embed to satisfy interfaces wholesale or compose behaviors
        (`struct { sync.Mutex; m map[string]int }` is fine for small unexported
        types; for exported types a named `mu sync.Mutex` field is better — embedding
        exports `Lock`/`Unlock` into your API).
      - **Never embed types whose method set becomes accidental public API**
        (embedding `*sql.DB` in your exported `Store` exposes all of it forever).
      - Embedding interfaces in structs to partially implement (test fakes:
        `struct{ storage.Store }` + override one method) is idiomatic in tests;
        in production code a nil embedded interface panics at runtime — audit.
      - Marshal trap: embedding flattens JSON fields and can silently change wire
        formats; promoted `MarshalJSON` from an embedded type hijacks the whole
        struct's encoding. MEDIUM if found on API types.
      
      ## 7. Functional options pattern
      
      Use when a constructor has ≥3 optional knobs or needs future extensibility
      without breaking callers. For ≤2 stable options, a config struct parameter or
      plain arguments are simpler — don't cargo-cult.
      
      ```go
      type Option func(*Server)
      
      func WithTimeout(d time.Duration) Option {
          return func(s *Server) { s.timeout = d }
      }
      func WithLogger(l *slog.Logger) Option {
          return func(s *Server) { s.log = l }
      }
      
      func New(addr string, opts ...Option) *Server {
          s := &Server{addr: addr, timeout: 30 * time.Second, log: slog.Default()}
          for _, o := range opts {
              o(s)
          }
          return s
      }
      ```
      
      Rules: required params are positional args, never options; every option has a
      sane default; options validate or the constructor returns `error` if
      combinations can be invalid; for cross-package extensibility use
      `Option interface{ apply(*config) }` instead of a bare func type.
      
      ## 8. context.Context discipline
      
      - **First parameter, named `ctx`, of every function on a request/IO path.**
        Not last, not in a struct, not optional.
      - **Never store ctx in a struct.** A struct outlives requests; storing ctx
        ties the object to one call's lifetime and hides cancellation flow. The
        known exceptions (`http.Request`) exist for compatibility — don't copy them.
        Audit `ctx context.Context` struct fields as MEDIUM.
      - **Values: request-scoped metadata only** — trace ID, auth principal,
        deadline-irrelevant telemetry. Never dependencies (DB handles, loggers as
        the *only* way to get them), never function parameters in disguise. If
        removing the value breaks business logic, it was a parameter.
      - Use **unexported key types** to avoid collisions:
      
      ```go
      type ctxKey struct{}
      func WithUser(ctx context.Context, u *User) context.Context {
          return context.WithValue(ctx, ctxKey{}, u)
      }
      func UserFrom(ctx context.Context) (*User, bool) {
          u, ok := ctx.Value(ctxKey{}).(*User)
          return u, ok
      }
      ```
      
      - Pass `ctx` down every blocking call: DB (`QueryContext`), HTTP
        (`http.NewRequestWithContext`), exec (`exec.CommandContext`). A blocking
        call without ctx on a request path is a HIGH leak/latency hazard.
      - `context.Background()` only in `main`, init paths, and tests
        (`t.Context()` in 1.24+); `context.TODO()` is a tracked refactor marker —
        audit lingering TODOs as LOW.
      - Always `defer cancel()` from `WithTimeout`/`WithCancel` (vet's
        `lostcancel` catches misses).
      - Detach correctly: to outlive a request (async audit log), use
        `context.WithoutCancel(ctx)` (1.21+) — keeps values, drops cancellation —
        with your own timeout. Don't pass the request ctx into background work
        (dies with request) and don't pass `Background()` (loses trace metadata).
      - `ctx.Err()` after select; return it unwrapped or wrapped with `%w` so
        callers can `errors.Is(err, context.Canceled)`.
      
      ## Audit checklist
      
      ```bash
      # Grab-bag packages — LOW
      find . -type d | grep -iE '/(util|utils|common|helpers|shared|misc)($|/)'
      
      # Returned interfaces from constructors (manual review) — LOW
      grep -rnE 'func New\w*\([^)]*\) [A-Z]\w*(Interface| interface)' --include='*.go' .
      
      # Fat interfaces: >4 methods (then inspect)
      grep -rn -A 12 'interface {' --include='*.go' . | less   # manual
      
      # Package-level mutable state — MEDIUM
      grep -rnE '^var \w+ (=|\*|map\[|\[\])' --include='*.go' . | grep -vE '(Err|_test|MustCompile|regexp)'
      grep -rn 'func init()' --include='*.go' .
      
      # Context violations
      grep -rnE 'ctx\s+context\.Context' --include='*.go' . | grep -E 'struct|^\s+[A-Za-z]+ +context\.Context'  # ctx in struct — MEDIUM
      grep -rnE 'func [^(]*\([^)]*\bctx context\.Context' --include='*.go' . | grep -vE '\(ctx context\.Context'  # ctx not first param
      grep -rn 'context.WithValue' --include='*.go' .          # check key types & payloads
      grep -rn 'context.TODO()' --include='*.go' .             # LOW, should be tracked
      grep -rnE 'context\.Background\(\)' --include='*.go' . | grep -v 'main\|_test'  # suspicious mid-stack
      
      # Blocking calls missing ctx variants — HIGH on request paths
      grep -rnE '\.(Query|QueryRow|Exec)\(' --include='*.go' . | grep -v Context
      grep -rn 'http.Get(\|http.Post(' --include='*.go' .
      grep -rn 'exec.Command(' --include='*.go' . | grep -v CommandContext
      
      # Naming drift — INFO
      grep -rnE 'func.*Get[A-Z]\w*\(\) ' --include='*.go' .    # Get-prefixed getters
      grep -rnE '\b(Id|Url|Http|Api)\b' --include='*.go' .     # acronym casing
      
      # Tooling
      go vet ./...                                  # lostcancel, composites
      golangci-lint run --enable-only revive,ireturn,containedctx,contextcheck,fatcontext ./...
      staticcheck ./...                             # ST1003 naming, S1021, SA1029 ctx keys
      ```
      
      Severity guide: ctx in struct / dependency-in-ctx MEDIUM; missing
      ctx on blocking request-path call HIGH; util-package and returned
      interfaces LOW; mutable package state MEDIUM (HIGH if written concurrently).
      
    • 03-concurrency.md 11.6 KB
      # 03 — Concurrency: goroutines, channels, sync, races
      
      Concurrency bugs are the most expensive class of Go defect: they pass review,
      pass tests, and corrupt data or leak memory in production. The discipline is
      ownership: every goroutine, channel, and shared variable has exactly one
      defined owner and lifecycle.
      
      ## 1. Goroutine lifecycle ownership
      
      **Before writing `go`, answer three questions in code, not in your head:**
      
      1. **When does it exit?** (ctx canceled, input channel closed, work done)
      2. **How do we wait for it?** (`errgroup.Wait`, `sync.WaitGroup`, join channel)
      3. **Where does its error/panic go?** (errgroup, error channel, recover+log)
      
      If any answer is "it doesn't / we don't", that's a leak by design — HIGH.
      
      ```go
      // BAD — fire-and-forget: no exit signal, no join, error lost
      go processQueue(q)
      
      // GOOD — owned: bounded by ctx, joined, error surfaced
      g, ctx := errgroup.WithContext(ctx)
      g.Go(func() error { return processQueue(ctx, q) })
      // ...
      if err := g.Wait(); err != nil { ... }
      ```
      
      - **A panic in any goroutine kills the process** — goroutines running
        panic-capable code (callbacks, plugins, parsers on untrusted input) need
        their own `defer func(){ if r := recover(); ... }()`.
      - Libraries must not spawn goroutines that outlive the call unless the API
        has an explicit `Close`/`Shutdown` that joins them.
      - Never use `time.Sleep` to "wait for the goroutine to start/finish" — that's
        a race with a timer on it. Synchronize with channels/WaitGroup. In tests on
        1.25+, `testing/synctest` gives you a fake-time bubble instead of sleeps.
      
      ## 2. Goroutine leak catalog
      
      Each pattern below leaks the goroutine (and everything it references) forever.
      
      **Blocked send, receiver gone** — classic in "first result wins" and timeouts:
      
      ```go
      // BAD — if caller times out, the worker blocks on send forever
      func fetch() chan result {
          ch := make(chan result)
          go func() { ch <- slowCall() }() // leak when nobody receives
          return ch
      }
      
      // FIX — buffer of 1 (send always completes) or select on ctx.Done()
      ch := make(chan result, 1)
      // or:
      select {
      case ch <- slowCall():
      case <-ctx.Done():
      }
      ```
      
      **Forgotten receiver / abandoned range** — producer ranges forever on a
      channel nobody closes, or consumer ranges a channel whose producer errored
      out before closing:
      
      ```go
      // BAD — if produce() returns early on error without close(ch), this blocks forever
      for v := range ch { ... }
      
      // FIX — producer owns the channel: defer close(ch) unconditionally
      go func() {
          defer close(ch)
          for _, v := range items {
              select {
              case ch <- v:
              case <-ctx.Done():
                  return
              }
          }
      }()
      ```
      
      **Missing ctx cancellation** — goroutine blocks on I/O or a channel with no
      `<-ctx.Done()` branch. Every blocking select in a spawned goroutine must
      include the done channel.
      
      **`time.After` in a loop** — pre-1.23, each call allocates a timer not
      collected until it fires; in a hot select loop this is unbounded memory.
      Since Go 1.23 unreferenced timers are collectable, so it's no longer a leak —
      but it still allocates per iteration; `time.NewTimer` + `Reset` or
      `time.Tick`/`time.NewTicker` remains correct for loops:
      
      ```go
      // BAD (pre-1.23 leak; ≥1.23 still allocates per iteration)
      for {
          select {
          case m := <-in:
              handle(m)
          case <-time.After(timeout):
              return
          }
      }
      
      // GOOD
      t := time.NewTimer(timeout)
      defer t.Stop()
      for {
          select {
          case m := <-in:
              handle(m)
              t.Reset(timeout)
          case <-t.C:
              return
          }
      }
      ```
      
      **Detection**: goroutine count metric (`runtime.NumGoroutine`) trending up;
      `pprof/goroutine?debug=2` dumps; `goleak` (`go.uber.org/goleak`) in tests:
      `defer goleak.VerifyNone(t)` — make it standard in packages that spawn.
      Go 1.26 adds an experimental `goroutineleak` pprof profile
      (build with `GOEXPERIMENT=goroutineleakprofile`, fetch
      `/debug/pprof/goroutineleak`) that reports goroutines blocked on unreachable
      concurrency primitives — planned on-by-default in 1.27; use it where available.
      
      ## 3. Channels vs mutexes
      
      Decision rule: **mutex for state, channels for handoff/signaling.**
      
      - Protecting a map/counter/struct field → `sync.Mutex`/`RWMutex`. A channel
        "manager goroutine" for simple state is slower, harder to read, and
        deadlock-prone.
      - Transferring ownership of data, pipelines, fan-out/fan-in, completion
        signals, semaphores → channels.
      - If you're tempted by both, the simpler mutex wins.
      
      Channel rules:
      
      - **The producer (writer) closes; never the consumer.** Closing is a
        broadcast: "no more values". Send on closed channel panics; close of closed
        channel panics; receive from closed channel returns zero immediately —
        `v, ok := <-ch` to distinguish.
      - Multiple producers: nobody closes directly; coordinate via a `sync.WaitGroup`
        + a single closer goroutine, or signal with a separate `done` channel.
      - Buffer sizes are 0 (synchronization), 1 (async handoff/notification), or a
        measured capacity (bounded queue). Any other magic number needs a comment;
        "buffered so it probably won't block" is a latent leak — MEDIUM.
      - `nil` channel in select disables that case — idiomatic for toggling cases
        off, surprising otherwise:
      
      ```go
      // Idiomatic: disable send case once drained
      var out chan<- Item
      if len(pending) > 0 { out = outCh } else { out = nil }
      select {
      case out <- pending[0]: ...
      case v := <-in: ...
      }
      ```
      
      - Don't use channels as mutexes (`make(chan struct{}, 1)` as a lock) unless
        you need ctx-aware locking (`select` on acquire) — then comment it.
      
      ## 4. sync primitives
      
      - **`errgroup` is the default for fan-out** (`golang.org/x/sync/errgroup`):
        first error cancels the derived ctx, `Wait` joins and returns it,
        `SetLimit(n)` bounds concurrency. Prefer it over hand-rolled
        WaitGroup+error-channel every time:
      
      ```go
      g, ctx := errgroup.WithContext(ctx)
      g.SetLimit(8)
      for _, u := range urls {
          g.Go(func() error { return fetch(ctx, u) }) // 1.22+: u is per-iteration
      }
      if err := g.Wait(); err != nil { return err }
      ```
      
      - **WaitGroup pitfalls**: `Add` before `go` (never inside the goroutine —
        race with `Wait`); never copy a WaitGroup (pass pointer; vet `copylocks`
        catches it); don't reuse before `Wait` returns. Go 1.25 adds `wg.Go(fn)`
        which does Add/Done correctly — prefer it where available.
      - **`sync.Once`** for lazy init; `sync.OnceValue/OnceValues/OnceFunc` (1.21+)
        are cleaner. Beware: if the once-fn panics, Once is consumed — subsequent
        calls return without retrying. For retryable init, use mutex + done flag.
      - **`sync.RWMutex`**: only when profiling shows read contention; RLock is not
        free and write starvation is real. Default to plain Mutex.
      - **`sync.Map`** is a niche tool (append-mostly caches, disjoint key sets);
        a mutex-guarded map is the default and is type-safe.
      - **`atomic`**: use the typed API (`atomic.Int64`, `atomic.Bool`,
        `atomic.Pointer[T]`) — it can't be misused with mixed atomic/non-atomic
        access the way `atomic.AddInt64(&x, 1)` on a plain int can. Atomics are for
        counters/flags; multi-field invariants need a mutex.
      - **Never copy a struct containing a mutex/WaitGroup/Cond** after first use —
        methods needing the lock take pointer receivers; `go vet` `copylocks`.
      
      ## 5. Data races
      
      A data race is undefined behavior in Go — not "stale reads", actual memory
      corruption is possible (interface values, slice headers, maps).
      
      - **Race detector in CI, always**: `go test -race ./...`. Run race-enabled
        binaries in staging/canary if feasible (`go build -race`, ~2-10x CPU,
        5-10x memory). A `-race` report is CRITICAL — there are no benign races.
      - **Loop variable capture**: since Go 1.22 (`go 1.22`+ in go.mod), `for`
        variables are per-iteration — the classic `go func(){ use(v) }()` bug is
        fixed *only if* the module's `go` directive is ≥1.22. Auditing a module
        with `go 1.21` or lower: every closure over a loop variable is suspect
        (HIGH). Check `go.mod` first.
      - **Concurrent map access**: unsynchronized read+write throws
        `fatal error: concurrent map writes` (unrecoverable, no recover) — or
        worse, races undetected. Any map reachable from multiple goroutines needs a
        mutex or redesign.
      - **Lazy "checked" init** (`if m == nil { m = make(...) }` from multiple
        goroutines), bool flags as ad-hoc signaling, and stat counters
        (`count++`) are all races even when "writes are rare".
      - HTTP handlers run concurrently: any handler touching shared server fields
        without sync is racy — top audit target.
      
      ## 6. Bounding concurrency: pools and semaphores
      
      **Unbounded `go` per work item is a self-DoS** (memory, FD, downstream
      overload). Bound everything fed by external input.
      
      - First choice: `errgroup` with `SetLimit` (above).
      - Channel semaphore when you need ctx-aware acquire or weighted slots
        (`golang.org/x/sync/semaphore` for weights):
      
      ```go
      sem := make(chan struct{}, maxInFlight)
      for _, job := range jobs {
          select {
          case sem <- struct{}{}:
          case <-ctx.Done():
              return ctx.Err()
          }
          go func() {
              defer func() { <-sem }()
              process(ctx, job)
          }()
      }
      ```
      
      - Worker pool (long-lived workers + job channel) only when worker setup is
        expensive (per-worker connections, caches) or you need strict FIFO; for
        plain throughput bounding, errgroup is less code and self-draining:
      
      ```go
      jobs := make(chan Job)
      g, ctx := errgroup.WithContext(ctx)
      for range numWorkers {
          g.Go(func() error {
              for j := range jobs {
                  if err := handle(ctx, j); err != nil { return err }
              }
              return nil
          })
      }
      // feed then close — producer owns the channel
      go func() { defer close(jobs); for _, j := range all { jobs <- j } }()
      err := g.Wait()
      ```
      
      - GOMAXPROCS: 1.25+ respects container CPU quotas automatically; on older
        runtimes in containers, `go.uber.org/automaxprocs` or explicit setting
        prevents throttling-induced latency.
      
      ## Audit checklist
      
      ```bash
      # go.mod language version — decides loop-var semantics (HIGH if <1.22 with closures in loops)
      grep -E '^go ' go.mod
      
      # Fire-and-forget goroutines — review each: exit path? join? error?
      grep -rn 'go func' --include='*.go' . | grep -v _test.go
      grep -rnE '^\s*go [a-zA-Z]' --include='*.go' .
      
      # Goroutines without ctx plumbed (manual: does the func take/select on ctx?)
      grep -rn -A3 'go func()' --include='*.go' . | grep -L 'ctx'
      
      # time.After in loops — MEDIUM (HIGH pre-1.23)
      grep -rn -B5 'time.After' --include='*.go' . | grep -E 'for |select'
      
      # Unbounded fan-out: go inside range — HIGH if input is external
      grep -rn -B2 'go func' --include='*.go' . | grep 'for .*range'
      
      # WaitGroup misuse: Add inside goroutine, copied WG
      grep -rn -A2 'go func' --include='*.go' . | grep 'wg.Add'
      go vet ./...                                  # copylocks, loopclosure (pre-1.22)
      
      # Raw atomic on plain ints (prefer atomic.Int64 types)
      grep -rnE 'atomic\.(Add|Load|Store|Swap)(Int|Uint|Pointer)' --include='*.go' .
      
      # sync.Map usage — verify it matches its niche
      grep -rn 'sync.Map' --include='*.go' .
      
      # Consumer-side close, double close candidates
      grep -rn 'close(' --include='*.go' .          # verify producer owns each
      
      # Sleep-based synchronization — MEDIUM (flaky + racy)
      grep -rn 'time.Sleep' --include='*.go' . | grep _test.go
      
      # Race detector + leak detection — non-negotiable
      go test -race ./...
      go test -race -count=5 ./...                  # shake out flaky interleavings
      grep -rn 'goleak' --include='*_test.go' .     # present in goroutine-spawning pkgs?
      
      # Runtime evidence (live systems)
      curl -s localhost:6060/debug/pprof/goroutine?debug=2 | head -100
      ```
      
      Severity guide: confirmed race CRITICAL; goroutine leak / unbounded fan-out
      on request path HIGH; missing `-race` in CI HIGH; `time.After` loop MEDIUM;
      hand-rolled errgroup-equivalent LOW.
      
    • 04-http-services.md 12.3 KB
      # 04 — HTTP & services: timeouts, shutdown, middleware, slog
      
      `net/http` defaults are tuned for compatibility, not production: zero server
      timeouts, infinite client waits, unlimited header sizes. Every production
      service overrides them. This file covers server, client, lifecycle, and
      observability.
      
      ## 1. http.Server — set ALL the timeouts
      
      `http.ListenAndServe(addr, h)` is unfit for production: no timeouts means any
      slow/malicious client (slowloris) holds a connection and goroutine forever —
      HIGH finding on any internet-facing service.
      
      ```go
      // GOOD — every production server
      srv := &http.Server{
          Addr:              ":8080",
          Handler:           mux,
          ReadHeaderTimeout: 5 * time.Second,   // slowloris defense; cheap, always set
          ReadTimeout:       10 * time.Second,  // full request incl. body
          WriteTimeout:      30 * time.Second,  // from end of headers (HTTP/1.1) to last byte written
          IdleTimeout:       120 * time.Second, // keep-alive connections between requests
          MaxHeaderBytes:    1 << 20,           // default 1MB; set explicitly
      }
      ```
      
      What each guards:
      
      | Timeout | Covers | If unset |
      |---|---|---|
      | `ReadHeaderTimeout` | Client sending request headers | Slowloris: drip headers forever |
      | `ReadTimeout` | Headers + entire body read | Slow body upload pins the conn |
      | `WriteTimeout` | Writing the response | Slow reader pins handler + buffers |
      | `IdleTimeout` | Keep-alive idle gap | Falls back to ReadTimeout; if both 0, idle conns live forever |
      
      - Large uploads/downloads/streaming/SSE: coarse `ReadTimeout`/`WriteTimeout`
        kill legitimate transfers. Use per-route control:
        `http.TimeoutHandler(h, d, msg)` for handler deadlines,
        `rc := http.NewResponseController(w); rc.SetWriteDeadline(...)` (1.20+) to
        extend deadlines per request. Keep `ReadHeaderTimeout` regardless.
      - Per-request deadlines for *work* belong in ctx:
        `context.WithTimeout(r.Context(), d)` around downstream calls. Server
        timeouts protect the transport; ctx protects the business logic.
      - Body limits: `http.MaxBytesReader(w, r.Body, maxSize)` on every endpoint
        that reads a body — unbounded `io.ReadAll(r.Body)` is a memory DoS (HIGH).
      - Routing: stdlib `http.ServeMux` (1.22+) supports methods and wildcards —
        `mux.HandleFunc("GET /users/{id}", h)`, `r.PathValue("id")`. Default to it;
        reach for chi/echo only for needed extras (route-scoped middleware trees).
      - Go 1.25+: `http.CrossOriginProtection` gives stdlib CSRF protection via
        Sec-Fetch-Site; use it or an equivalent for cookie-authenticated mutations.
      
      ## 2. HTTP clients — timeouts, body hygiene, reuse
      
      **`http.DefaultClient` has NO timeout** — a hung server hangs your goroutine
      forever. Never use `http.Get/Post/...` package functions in services (HIGH).
      
      ```go
      // GOOD — explicit client, reused (it's goroutine-safe; pools connections)
      client := &http.Client{
          Timeout: 10 * time.Second, // absolute cap: dial+TLS+request+read body
      }
      
      // Per-attempt control with ctx (preferred for request-scoped deadlines)
      ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
      defer cancel()
      req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
      ```
      
      `Client.Timeout` includes reading the body; for streaming responses, leave it
      0 and use ctx + `Transport` knobs instead:
      
      ```go
      t := &http.Transport{
          Proxy:                 http.ProxyFromEnvironment,
          DialContext:           (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
          TLSHandshakeTimeout:   5 * time.Second,
          ResponseHeaderTimeout: 5 * time.Second,
          ExpectContinueTimeout: 1 * time.Second,
          MaxIdleConns:          100,
          MaxIdleConnsPerHost:   100, // DEFAULT IS 2 — throttles any single-host workload
          IdleConnTimeout:       90 * time.Second,
      }
      client := &http.Client{Transport: t, Timeout: 10 * time.Second}
      ```
      
      `DefaultMaxIdleConnsPerHost = 2` is the classic hidden bottleneck for
      service-to-service traffic: connections churn, ports exhaust (TIME_WAIT),
      latency spikes. Set `MaxIdleConnsPerHost` ≈ peak concurrency to that host.
      
      **Body discipline — every response, every path:**
      
      ```go
      resp, err := client.Do(req)
      if err != nil {
          return err // resp is nil on error; do NOT touch resp.Body here
      }
      // Connection reuse requires the body fully read before Close. Defers run
      // LIFO: register Close first so the bounded drain executes before it.
      defer resp.Body.Close()
      defer io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
      ```
      
      Unclosed bodies leak FDs and goroutines;
      undrained bodies kill connection reuse (LOW perf, MEDIUM at scale). Always
      check `resp.StatusCode` — `err == nil` for 4xx/5xx.
      
      Create **one client per upstream at startup**, inject it; never build a
      client (or Transport) per request — each Transport owns a fresh pool.
      
      ## 3. Graceful shutdown
      
      Pattern: catch signals via ctx, stop accepting, drain in-flight with a
      deadline, then close dependencies.
      
      ```go
      func run(ctx context.Context) error {
          ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
          defer stop()
      
          srv := &http.Server{ /* ...timeouts as §1... */ }
          errCh := make(chan error, 1)
          go func() { errCh <- srv.ListenAndServe() }()
      
          select {
          case err := <-errCh:
              return fmt.Errorf("server: %w", err) // ListenAndServe always returns non-nil
          case <-ctx.Done():
          }
      
          shCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
          defer cancel()
          if err := srv.Shutdown(shCtx); err != nil {       // stops Accept, waits for handlers
              return fmt.Errorf("shutdown: %w", err)        // DeadlineExceeded => srv.Close() already forced
          }
          return nil
      }
      ```
      
      - `Shutdown` does NOT cancel handler contexts or wait for hijacked conns
        (WebSockets); use `srv.RegisterOnShutdown` to signal those, and propagate a
        "draining" ctx to long-running handlers.
      - `http.ErrServerClosed` from `ListenAndServe` after Shutdown is expected —
        filter it: `if !errors.Is(err, http.ErrServerClosed)`.
      - Shutdown deadline must be **shorter than** the orchestrator's kill grace
        period (K8s `terminationGracePeriodSeconds`, default 30s) and account for
        readiness-probe propagation: flip readiness to failing first, sleep a
        beat (or rely on `preStop`), then Shutdown — otherwise traffic still
        arrives at a closed listener.
      - Close order after drain: server → background workers (cancel + join) →
        DB pools/queues → flush telemetry. Reverse of startup.
      
      ## 4. Middleware
      
      Standard shape — `func(http.Handler) http.Handler`, composed outermost-first:
      
      ```go
      func RequestLogger(log *slog.Logger) func(http.Handler) http.Handler {
          return func(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  start := time.Now()
                  ww := &statusWriter{ResponseWriter: w, status: http.StatusOK}
                  next.ServeHTTP(ww, r)
                  log.LogAttrs(r.Context(), slog.LevelInfo, "request",
                      slog.String("method", r.Method),
                      slog.String("path", r.URL.Path),
                      slog.Int("status", ww.status),
                      slog.Duration("dur", time.Since(start)),
                  )
              })
          }
      }
      
      type statusWriter struct {
          http.ResponseWriter
          status int
      }
      func (w *statusWriter) WriteHeader(c int) { w.status = c; w.ResponseWriter.WriteHeader(c) }
      ```
      
      - Order matters: recover (outermost) → request ID/trace → logging → auth →
        rate limit → handler. Recovery middleware logs `debug.Stack()` and returns
        500; check `errors.Is(err, http.ErrAbortHandler)` style sentinel —
        re-panic `http.ErrAbortHandler` rather than swallowing it.
      - Wrapper `ResponseWriter`s hide optional interfaces (`http.Flusher`,
        `http.Hijacker`); implement passthroughs or use
        `http.NewResponseController(w)` (1.20+), which unwraps automatically — SSE
        and WebSockets break otherwise (MEDIUM).
      - Don't read `r.Body` in middleware unless you replace it
        (`r.Body = io.NopCloser(bytes.NewReader(buf))`) — handlers get an empty body.
      - Prefer returning errors from handlers via a small adapter
        (`func(w, r) error` → `http.Handler`) so the error mapper from
        `rules/01 §7` is the single response-shaping point.
      
      ## 5. Structured logging with slog
      
      `log/slog` (1.21+) is the standard. `fmt.Println`/`log.Printf` in services is
      LOW debt; unstructured logs can't be queried.
      
      ```go
      log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
          Level: slog.LevelInfo,           // make it a flag/env; use slog.LevelVar for runtime changes
      }))
      slog.SetDefault(log)                  // also reroutes legacy log.Printf
      
      log.InfoContext(ctx, "payment processed",
          "order_id", orderID,             // alternating key/value
          slog.Int("attempts", n),         // or typed attrs — faster, type-safe
      )
      ```
      
      - **Always the `*Context` variants on request paths** — handlers can extract
        trace/span IDs from ctx (OTel bridges do).
      - Inject `*slog.Logger` as a dependency (constructor arg) in libraries;
        `slog.Default()` is acceptable at app level. Pre-bind request attrs once:
        `log = log.With("request_id", id)` in middleware, pass via ctx value or
        handler closure.
      - Hot paths: `log.LogAttrs(ctx, level, msg, attrs...)` avoids
        `[]any` allocs; guard expensive computation with
        `if log.Enabled(ctx, slog.LevelDebug)`.
      - Fan-out to several sinks (e.g. JSON to stdout + a file handler):
        `slog.NewMultiHandler(h1, h2)` (1.26+) replaces hand-rolled multi-handler
        wrappers and third-party equivalents.
      - Never log secrets/PII: implement `slog.LogValuer` on sensitive types to
        redact by construction:
      
      ```go
      func (t Token) LogValue() slog.Value { return slog.StringValue("REDACTED") }
      ```
      
      - Levels: Debug (dev diagnosis), Info (state changes worth auditing), Warn
        (degraded, self-healed), Error (failed operation, human may act). Don't log
        Error for client mistakes (4xx) — that's Info/Warn; alert noise kills oncall.
      
      ## 6. Request-scoped values
      
      - Request ID/trace context: set in middleware, store in ctx (unexported key —
        `rules/02 §8`), read everywhere via accessor funcs.
      - Auth principal: middleware authenticates, puts `*User`/claims in ctx;
        handlers call `auth.UserFrom(ctx)`. Handlers never re-parse tokens.
      - Everything else (parsed body, query params) is plain function arguments —
        ctx is not a parameter bag.
      - Propagate outbound: when calling downstream services pass `ctx` into
        `http.NewRequestWithContext` and inject trace headers (otelhttp transport
        does both).
      
      ## Audit checklist
      
      ```bash
      # Naked servers — HIGH
      grep -rn 'http.ListenAndServe\|http.ListenAndServeTLS' --include='*.go' .
      grep -rn -A8 'http.Server{' --include='*.go' .   # verify all four timeouts present
      
      # Default client / package-level helpers — HIGH
      grep -rnE 'http\.(Get|Post|PostForm|Head)\(' --include='*.go' .
      grep -rn 'http.DefaultClient' --include='*.go' .
      grep -rn -A6 'http.Client{' --include='*.go' .   # Timeout set? Transport tuned?
      grep -rn 'MaxIdleConnsPerHost' --include='*.go' .  # absent + high fan-out = bottleneck
      
      # Body hygiene
      grep -rn 'client.Do\|\.Get(\|\.Post(' --include='*.go' .   # then verify defer Close + drain near each
      grep -rn 'resp.Body.Close' --include='*.go' .
      grep -rn 'io.ReadAll(r.Body\|io.ReadAll(req.Body' --include='*.go' .  # MaxBytesReader present? — HIGH
      grep -rn 'MaxBytesReader' --include='*.go' .
      
      # Shutdown
      grep -rn 'signal.NotifyContext\|signal.Notify' --include='*.go' .
      grep -rn 'srv.Shutdown\|.Shutdown(' --include='*.go' .      # absent => no graceful drain — MEDIUM
      grep -rn 'ErrServerClosed' --include='*.go' .
      
      # Per-request client/transport construction — MEDIUM perf
      grep -rn -B3 'http.Client{' --include='*.go' . | grep -E 'func.*\(w http|Handler'
      
      # Logging
      grep -rnE '\b(fmt\.Print|log\.Print)' --include='*.go' . | grep -v _test.go   # LOW
      grep -rn 'slog.' --include='*.go' . | grep -v Context     # request paths should use *Context
      grep -rnE '(password|token|secret|authorization|api_?key)' --include='*.go' . | grep -i 'slog\|log\.'  # PII in logs — HIGH
      
      # Middleware ResponseWriter wrappers missing Flush/Hijack passthrough
      grep -rn -A4 'http.ResponseWriter$' --include='*.go' . | grep 'struct'
      
      # Tooling
      golangci-lint run --enable-only bodyclose,noctx,gosec ./...   # noctx: requests without ctx
      go vet ./...
      ```
      
      Severity guide: no server timeouts internet-facing HIGH; default client in
      service HIGH; unbounded body read HIGH; missing graceful shutdown MEDIUM;
      unclosed/undrained bodies MEDIUM; unstructured logging LOW; secrets in logs
      HIGH.
      
    • 05-security.md 16.9 KB
      # 05 — Security: injection, paths, TLS, integers, supply chain
      
      Threat model every input by origin: network, files, env, CLI args, DB
      contents, and inter-service messages are all untrusted until validated.
      Go removes memory-corruption classes but injection, traversal, SSRF, and
      misconfiguration are entirely yours.
      
      ## 1. Input validation at trust boundaries
      
      - Validate at the **boundary** (handler/consumer), once, into a typed value;
        interior code trusts its types. Re-validating everywhere means nobody knows
        where the real check is.
      - Allowlist over denylist: enums via `ParseX(string) (X, error)`, bounded
        lengths/sizes on every string/slice, `utf8.ValidString` on text that
        reaches storage or other parsers.
      - JSON: `dec := json.NewDecoder(r.Body); dec.DisallowUnknownFields()` for
        strict APIs; remember `encoding/json` ignores case in field matching and
        silently drops unknown fields by default — security-relevant for
        privilege fields. Pair with `http.MaxBytesReader` (`rules/04`).
      - Numbers from JSON into `any` become `float64` — large int64 IDs silently
        lose precision; decode into concrete struct types or `json.Number`.
      - Never echo raw input into errors/logs without bounding
        (`%.100q`) — log injection and PII leak.
      - Don't reflect internal errors to clients (`rules/01 §7`): stack traces,
        SQL text, and file paths in responses are recon gifts — MEDIUM.
      - Output encoding is contextual and separate from input validation: render
        HTML through `html/template` (it auto-escapes per context), never
        `text/template` or `fmt.Fprintf` into a page — that's stored/reflected XSS.
        The `template.HTML`/`JS`/`URL`/`CSS`/`HTMLAttr` cast types switch escaping
        off; each is a sink that must be independently sanitized. Full
        XSS/CSP/output-encoding rules live in sota-code-security `05`.
      
      ## 2. SQL — parameterized always
      
      String-built SQL is CRITICAL regardless of the value's provenance ("it's an
      int", "it's internal") — provenance changes, code is copied.
      
      ```go
      // BAD — CRITICAL
      q := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
      rows, err := db.Query(q)
      
      // GOOD — database/sql placeholders
      rows, err := db.QueryContext(ctx,
          "SELECT id, name FROM users WHERE email = $1", email)
      
      // GOOD — pgx native
      row := pool.QueryRow(ctx, "SELECT id FROM users WHERE email = $1", email)
      ```
      
      - **sqlc** is SOTA for query-heavy services: SQL in `.sql` files, generated
        type-safe Go, parameterization by construction, schema-checked at codegen.
        **pgx/v5** as the runtime driver/pool (`pgxpool`) for Postgres.
      - Identifiers (table/column names, ORDER BY direction) can't be placeholders:
        map them through a hardcoded allowlist, never interpolate input:
      
      ```go
      orderCol, ok := map[string]string{"name": "name", "created": "created_at"}[req.Sort]
      if !ok { return ErrBadSort }
      q := "SELECT ... ORDER BY " + orderCol // safe: values are program constants
      ```
      
      - `IN (...)` lists: build placeholders programmatically or use pgx's array
        binding (`= ANY($1)`).
      - LIKE patterns: escape `%`/`_` in user input before binding.
      - Always `defer rows.Close()`; check `rows.Err()` after the loop; use
        `QueryContext`/`ExecContext` (ctx discipline). Pool settings
        (`SetMaxOpenConns`, `SetMaxIdleConns`, `SetConnMaxLifetime`) explicit —
        default unlimited open conns can flatten the DB.
      - ORMs: if GORM/ent are in use, audit every `Raw`, `Exec`, `Where(fmt.Sprintf`
        call site — string interpolation there is the same CRITICAL.
      
      ## 3. os/exec — argv, never shell
      
      `exec.Command` does NOT invoke a shell — that's the security feature. Each
      argument is a separate argv entry; metacharacters are inert.
      
      ```go
      // BAD — CRITICAL command injection
      out, err := exec.Command("sh", "-c", "convert "+userFile+" out.png").Output()
      
      // GOOD — argv vector, ctx-bound
      cmd := exec.CommandContext(ctx, "convert", userFile, "out.png")
      cmd.Stdout, cmd.Stderr = &outBuf, &errBuf
      err := cmd.Run()
      ```
      
      - `sh -c` / `bash -c` with ANY interpolated data is CRITICAL. With constant
        strings only, it's MEDIUM (fragile pattern, invites edits that interpolate).
      - Argument injection still applies: a userFile like `-trim` becomes a flag.
        Use `--` end-of-options where the tool supports it, validate the value
        shape, or prefix paths (`./`+name).
      - Set `cmd.Dir`, pass minimal `cmd.Env` (don't inherit secrets-laden environ
        into child processes: `cmd.Env = []string{"PATH=/usr/bin"}`).
      - Binary resolution: Go 1.19+ `exec.LookPath`/`Command` refuse relative-path
        matches from the current directory on Unix; still prefer absolute paths
        for security-sensitive helpers.
      - `CommandContext` kills on ctx cancel but **waits for copied pipes**; set
        `cmd.WaitDelay` (1.20+) so a child that inherits the pipe can't block
        `Wait` forever.
      
      ## 4. Path traversal
      
      Joining user input into paths without containment is HIGH/CRITICAL
      (read: arbitrary file read/write).
      
      ```go
      // BAD — ../../../etc/passwd
      f, err := os.Open(filepath.Join(baseDir, userPath))
      ```
      
      **Go 1.24+: `os.Root` is the answer** — kernel-enforced containment
      (openat2/RESOLVE_BENEATH semantics), immune to `..`, absolute paths, and
      symlink escapes:
      
      ```go
      root, err := os.OpenRoot(baseDir)
      if err != nil { return err }
      defer root.Close()
      f, err := root.Open(userPath) // cannot escape baseDir, even via symlinks
      ```
      
      Caveat: containment is only as good as the toolchain — CVE-2026-39822
      (fixed in 1.26.5/1.25.12, 2026-07) let `root.Open("symlink/")` follow a
      trailing-slash symlink out of the Root on Unix (go.dev issue #79005).
      Reinforces the §8 keep-the-toolchain-current rule.
      
      Pre-1.24 fallback (and for path *strings* not yet opened):
      
      ```go
      func securePath(baseDir, userPath string) (string, error) {
          if !filepath.IsLocal(userPath) { // 1.20+: rejects ../, absolute, reserved names
              return "", fmt.Errorf("invalid path %q", userPath)
          }
          return filepath.Join(baseDir, userPath), nil
      }
      ```
      
      - `filepath.Clean` alone is NOT containment (it normalizes; `Clean("../x")`
        is still `../x`). Prefix-checking `strings.HasPrefix(abs, base)` misses
        symlinks and `base`-sibling prefixes (`/srv/app` vs `/srv/app-secrets`) —
        if you must, compare against `filepath.Separator`-terminated resolved
        (`filepath.EvalSymlinks`) paths.
      - Zip/tar extraction: validate every entry name with `filepath.IsLocal` +
        reject absolute/`..` (zip-slip); bound total size and file count
        (decompression bomb).
      - Serving files: `http.ServeFile` rejects `..` but build the path with
        `http.FileServer`/`http.FS` over a rooted FS rather than manual joins;
        `os.DirFS` + `fs.Sub`, or `os.Root.FS()` on 1.24+.
      
      ## 5. Integer conversion overflow (gosec G115)
      
      Conversions between int sizes/signs silently truncate/wrap — exploitable when
      the value gates an allocation, length, offset, or privilege check.
      
      ```go
      // BAD — attacker sends length = 4294967296; on 32-bit int wraps to 0
      n := int32(req.Length)          // truncates
      buf := make([]byte, n)
      
      // BAD — negative int → huge uint
      u := uint64(off)                // off = -1 → 18446744073709551615
      
      // GOOD — bounds-check before every narrowing/sign-changing conversion
      if req.Length < 0 || req.Length > math.MaxInt32 {
          return fmt.Errorf("length %d out of range", req.Length)
      }
      n := int32(req.Length)
      ```
      
      - High-risk sites: `len()` math into smaller ints, binary protocol parsing,
        `strconv.Atoi` (returns platform `int`) then narrowed — use
        `strconv.ParseInt(s, 10, 32)` with the explicit bit size instead.
      - gosec rule G115 flags these; triage rather than blanket-`nolint`. For
        hot paths a tiny generic helper (`func conv[T, U constraints.Integer]`)
        centralizes the check.
      - Durations: `time.Duration(n) * time.Second` where `n` is attacker-supplied
        can overflow int64 — bound first.
      
      ## 6. Cryptographic practices: CSPRNG & TLS
      
      ### Randomness — `crypto/rand`, never `math/rand`
      
      Security-bearing randomness — tokens, session IDs, password-reset codes, API
      keys, salts, nonces, IVs — MUST come from `crypto/rand`. `math/rand` *and*
      `math/rand/v2` are PRNGs whose "outputs might be easily predictable regardless
      of how it's seeded" (package docs); using either for anything an attacker must
      not guess is HIGH (CRITICAL when it gates auth — a guessable reset token is
      account takeover).
      
      ```go
      // BAD — predictable; applies to math/rand and math/rand/v2 alike
      token := strconv.FormatInt(mrand.Int63(), 36)
      
      // GOOD — Go 1.24+: ready-made secret string (RFC 4648 base32, ≥128 bits)
      token := rand.Text()                  // crypto/rand.Text
      
      // GOOD — raw bytes; Read never errors and always fills b entirely
      b := make([]byte, 32)
      rand.Read(b)                          // crypto/rand.Read
      ```
      
      - `crypto/rand.Read`/`Text` cannot hand back a short or weak read — on a
        failing source they crash the program rather than degrade. Don't wrap them in
        `if err != nil` logic that silently falls back to `math/rand`.
      - Watch the import line, not just the call: `math/rand.Read` is **deprecated**
        precisely because an unqualified `rand.Read` under `import "math/rand"`
        reads like the crypto one but isn't. Grep imports, then call sites.
      - `math/rand/v2` is the right tool — and the better PRNG API — for *non-secret*
        work: jitter, load distribution, sampling, test fixtures. The dividing line
        is "does predictability help an attacker", not "which package is newer".
      - Keys, signing, AEAD: use `crypto/*` (`ed25519`, `crypto/ecdsa`,
        `crypto/aes` + `cipher.NewGCM`) drawing from `crypto/rand.Reader`; never
        hand-roll. Password hashing is `golang.org/x/crypto/bcrypt`/`argon2` —
        algorithm choice and parameters are owned by sota-code-security `04`.
      
      ### TLS configuration
      
      Go's `crypto/tls` defaults are good (1.22+ defaults to strong suites; 1.24+
      enables post-quantum X25519MLKEM768 key exchange, and 1.26 also enables
      SecP256r1MLKEM768/SecP384r1MLKEM1024 by default; the legacy GODEBUG opt-outs
      `tlsrsakex`/`tls10server`/`tls3des` are slated for removal in 1.27). The main
      sins are *downgrades*:
      
      ```go
      // CRITICAL — disables all certificate verification
      cfg := &tls.Config{InsecureSkipVerify: true}
      
      // GOOD — modern floor, otherwise stdlib defaults
      cfg := &tls.Config{MinVersion: tls.VersionTLS12} // TLS13 for internal-only
      ```
      
      - `InsecureSkipVerify: true` is CRITICAL anywhere near production code paths,
        including "temporary" test toggles compiled into the binary. Pinning or
        custom CA? Use `RootCAs` with the CA pool, or `VerifyPeerCertificate` with
        real verification — never blanket skip.
      - Don't set `CipherSuites`/`CurvePreferences` manually unless compliance
        forces it — stale hand-picked lists rot; stdlib defaults track best
        practice per release.
      - mTLS: server `ClientAuth: tls.RequireAndVerifyClientCert` + `ClientCAs`.
      - Plain `http://` to internal services carrying credentials is HIGH unless
        the transport is otherwise authenticated/encrypted (mTLS mesh).
      
      ## 7. unsafe and cgo policy
      
      - `unsafe`: forbidden outside a designated, documented, owner-reviewed
        package. Each use carries a comment proving the invariant (per
        `unsafe.Pointer` rules) and a fuzz/race-tested wrapper. Audit any new
        `unsafe.Pointer` arithmetic as HIGH until proven.
      - `//go:linkname`, `reflect.SliceHeader`/`StringHeader` (deprecated): treat
        as `unsafe`; 1.20+ `unsafe.String/StringData/Slice/SliceData` are the only
        sanctioned forms.
      - cgo: each C dependency reintroduces memory-unsafety, complicates
        cross-compilation and static linking, and bypasses govulncheck's call
        analysis. Require justification (no pure-Go alternative), pin and scan the
        C library separately, and isolate behind one package. `CGO_ENABLED=0` for
        builds unless cgo is required.
      
      ## 8. Supply chain & vuln management
      
      - **`govulncheck ./...` in CI** (it's call-graph aware — low false positives;
        also run `govulncheck -mode=binary` on shipped artifacts — it now checks the
        binary's main module too, and `-format sarif` feeds code-scanning UIs).
        Fails the build on findings; triage with documented suppressions, not
        removal of the step.
      - **The stdlib itself is the top vuln surface** — H1 2026 alone patched DoS
        CVEs in `crypto/tls` (CVE-2026-32283, TLS 1.3 key-update flood),
        `crypto/x509` (CVE-2026-32280, CVE-2026-27145) and `net/mail`
        (CVE-2026-42499); 1.26.5/1.25.12 (2026-07) fixed CVE-2026-39822 (os.Root
        symlink escape, §4) and CVE-2026-42505 (`crypto/tls` ECH leaked pre-shared
        key identities, de-anonymizing server hostnames). govulncheck only helps
        if the *toolchain* is current: track the monthly patch releases (verify
        the current level at go.dev/doc/devel/release) and rebuild/redeploy on
        security point releases.
      - **`go.sum` committed always**; builds verify hashes against it and the
        checksum DB (sum.golang.org), on by default. `GONOSUMDB` and `GONOPROXY`
        exempt matching module patterns from sumdb/proxy; `GOPRIVATE` sets both —
        use `GOPRIVATE=*.corp.example.com` for internal modules and nothing else.
        `GOSUMDB=off`, `GOFLAGS=-mod=mod` in CI, or wildcard `GONOSUMDB=*` disable
        verification repo-wide — HIGH finding. Everything public must flow through
        proxy.golang.org + sumdb verification.
      - **Minimal deps philosophy**: stdlib first; `golang.org/x/*` second; each
        third-party module needs maintenance signal (recent releases, issue
        hygiene), a license check, and a reason a 50-line vendored function can't
        replace it. Transitives count: `go mod graph | wc -l` before/after.
      - **Audit side — already-landed deps**: `go mod why -m <module>` prints
        `(main module does not need module …)` for an unreached one — verbatim, exit 0
        — but its graph includes tests of reachable packages (`-vendor` excludes tests
        *of dependencies*; your own test-only deps still read as reached). A tool's
        silence is not proof — the sweep, the deletion proof, and the leverage-ratio
        and upstream-health checks are `sota-devsecops` rules/10.
      - `go mod tidy` enforced in CI (`git diff --exit-code go.mod go.sum` after).
      - Pin tool versions via 1.24 `tool` directives in go.mod (`rules/07`) so the
        linter/codegen supply chain is hash-verified too.
      - Don't `replace` to forks silently — audit `replace` directives (MEDIUM:
        hidden fork drift; CRITICAL if pointing at an unreviewed repo).
      - Secrets: never in code/env-committed files; load via env/secret manager at
        start; `slog.LogValuer` redaction (`rules/04 §5`).
      
      ## Audit checklist
      
      ```bash
      # SQL injection — CRITICAL
      grep -rnE '(Sprintf|fmt\.Sprint|\+ ?\w+ ?\+).*((?i)select|insert|update|delete|where)' --include='*.go' .
      grep -rnE '(Query|Exec|QueryRow)[^(]*\(("[^"]*"\s*\+|fmt\.Sprintf)' --include='*.go' .
      grep -rnE '\.(Raw|Where)\(fmt\.Sprintf' --include='*.go' .       # GORM-style
      
      # Command injection — CRITICAL
      grep -rnE 'exec\.Command(Context)?\(\s*"(sh|bash|cmd|powershell)"' --include='*.go' .
      grep -rn 'exec.Command' --include='*.go' .                        # verify argv construction per site
      
      # Path traversal — HIGH
      grep -rnE 'filepath\.Join\([^)]*(r\.|req\.|input|name|param|id)' --include='*.go' .
      grep -rn 'os.Root\|filepath.IsLocal' --include='*.go' .           # mitigations present?
      go version   # os.Root containment needs >=1.26.5/1.25.12 — CVE-2026-39822 symlink escape
      grep -rnE 'os\.(Open|Create|ReadFile|WriteFile|Remove)' --include='*.go' . # trace path provenance
      
      # TLS — CRITICAL/HIGH
      grep -rn 'InsecureSkipVerify' --include='*.go' .
      grep -rnE 'MinVersion:\s*tls\.VersionTLS1[01]' --include='*.go' .
      grep -rn '"http://' --include='*.go' . | grep -v 'localhost\|127.0.0.1\|test'
      
      # Integer conversion — gosec G115
      grep -rnE '\b(int8|int16|int32|uint8|uint16|uint32|uint64|uintptr)\(' --include='*.go' . | grep -vE '(_test|const)'
      gosec -include=G115,G118,G201,G202,G204,G304,G401,G402 ./...
      # gosec 2.24+ adds G113 (request smuggling via conflicting headers),
      # G118 (ctx-propagation goroutine leaks), G408 (SSH PublicKeyCallback bypass)
      
      # CSPRNG misuse — HIGH (security-bearing randomness from a PRNG)
      grep -rn 'math/rand' --include='*.go' .                 # any import: verify each call site is non-secret
      grep -rnE '\brand\.(Int|Intn|Int31|Int63|Uint|Float|Perm|Shuffle|N)\b' --include='*.go' . # PRNG calls — crypto context?
      
      # Output encoding / XSS — HIGH
      grep -rn 'text/template' --include='*.go' . | grep -iv _test   # HTML rendered via text/template?
      grep -rnE 'template\.(HTML|JS|URL|CSS|HTMLAttr)\(' --include='*.go' . # escaping bypass — verify sanitized
      
      # unsafe / cgo
      grep -rn 'unsafe.Pointer\|go:linkname' --include='*.go' .
      grep -rln 'import "C"' --include='*.go' .
      
      # Supply chain
      test -f go.sum && echo OK || echo 'MISSING go.sum — HIGH'
      grep -E '^replace' go.mod
      go env GOFLAGS GONOSUMDB GOSUMDB GOPRIVATE GONOSUMCHECK 2>/dev/null
      go mod verify
      govulncheck ./...
      go mod tidy && git diff --exit-code go.mod go.sum
      
      # Secrets in repo
      grep -rnE '(api[_-]?key|secret|password|token)\s*[:=]\s*"[A-Za-z0-9+/_-]{16,}"' --include='*.go' .
      ```
      
      Severity guide: string-built SQL / `sh -c` with input / InsecureSkipVerify /
      `math/rand` for an auth-gating token CRITICAL; traversal-reachable file ops,
      disabled sumdb, unchecked narrowing on attacker-controlled sizes, `math/rand`
      for other secrets, HTML via `text/template` MEDIUM-to-HIGH; raw error echo to
      clients, silent `replace` MEDIUM.
      
    • 06-performance.md 11.5 KB
      # 06 — Performance: profiling, allocations, GC, PGO
      
      Rule zero: **measure before and after; optimize only what profiles indict.**
      Clarity-destroying micro-optimizations without a benchmark are LOW findings
      in audits, same as obvious waste on a measured hot path.
      
      ## 1. pprof workflow
      
      Wire profiling into every service (auth-gated or bound to localhost/admin
      port — exposing pprof publicly is a MEDIUM info-leak/DoS finding):
      
      ```go
      import _ "net/http/pprof" // registers on http.DefaultServeMux
      
      go func() { // separate internal-only listener, never the public mux
          slog.Error("pprof", "err", http.ListenAndServe("localhost:6060", nil))
      }()
      ```
      
      Capture and analyze:
      
      ```bash
      go tool pprof -http=:8081 'http://localhost:6060/debug/pprof/profile?seconds=30'  # CPU
      go tool pprof -http=:8081 'http://localhost:6060/debug/pprof/heap'                # live heap (inuse_space)
      go tool pprof -sample_index=alloc_space -http=:8081 '.../debug/pprof/heap'        # cumulative allocs
      curl -s 'localhost:6060/debug/pprof/goroutine?debug=2'                             # goroutine dump (leak hunt)
      go tool pprof '.../debug/pprof/mutex'     # contention; needs runtime.SetMutexProfileFraction(>0)
      go tool pprof '.../debug/pprof/block'     # blocking; needs runtime.SetBlockProfileRate(>0)
      curl -s 'localhost:6060/debug/pprof/goroutineleak'  # 1.26 experimental leak profile (GOEXPERIMENT=goroutineleakprofile)
      ```
      
      Reading order for a perf complaint: CPU profile (flame graph) → if CPU is in
      `runtime.mallocgc`/GC, switch to heap `alloc_space` to find allocation sites →
      if CPU is idle but latency high, goroutine/block/mutex profiles. `runtime/trace`
      (`go tool trace`) for scheduler-level mysteries: long GC pauses, goroutine
      starvation, syscall stalls. Continuous profiling (Parca/Pyroscope/Cloud
      Profiler) is SOTA for production — point-in-time pprof lies about spiky loads.
      Go 1.25 adds `trace.FlightRecorder` for capturing the moments *before* an
      anomaly.
      
      ## 2. Benchmarks with testing.B
      
      ```go
      func BenchmarkParse(b *testing.B) {
          data := loadFixture(b)      // setup outside the loop
          b.ReportAllocs()
          for b.Loop() {              // Go 1.24+: replaces `for i := 0; i < b.N; i++`
              parse(data)             // b.Loop prevents the compiler optimizing the call away
          }
      }
      ```
      
      - `b.Loop()` (1.24+) auto-excludes setup/teardown from timing and keeps the
        loop body from being dead-code-eliminated — the old `b.N` + global-sink
        idiom is obsolete; on older Go, assign results to a package-level sink.
        Go 1.26 fixed `b.Loop` blocking inlining in the loop body (which skewed
        allocs/op on 1.24/1.25) — on 1.26+ migrate all `b.N` benchmarks with no
        caveats.
      - Always `b.ReportAllocs()`; allocs/op regressions are the early warning.
      - Run with `-count=10` and compare using **benchstat** (significance, not
        vibes): `go test -bench=. -count=10 | benchstat old.txt new.txt`.
        Single-run deltas under ~5% are noise.
      - `-benchmem`, `-cpuprofile`/`-memprofile` on benchmarks feed pprof directly:
        `go test -bench=Parse -cpuprofile=cpu.out && go tool pprof cpu.out`.
      - Benchmark with realistic data sizes/shapes; `b.Run` sub-benchmarks over a
        size table exposes O(n²) cliffs.
      
      ## 3. Allocation reduction
      
      Allocations cost three times: malloc, GC mark, cache misses. The biggest wins:
      
      **Preallocate when length is known:**
      
      ```go
      // BAD — grows: repeated alloc+copy
      var out []Item
      for _, r := range rows { out = append(out, convert(r)) }
      
      // GOOD
      out := make([]Item, 0, len(rows))
      for _, r := range rows { out = append(out, convert(r)) }
      
      m := make(map[string]int, len(keys)) // maps too
      ```
      
      **String building** — `+` in a loop is O(n²) allocs:
      
      ```go
      var sb strings.Builder
      sb.Grow(estimatedLen)          // one alloc if estimate holds
      for _, p := range parts { sb.WriteString(p) }
      s := sb.String()               // no copy: Builder transfers ownership
      ```
      
      Also: `strconv.AppendInt(buf, n, 10)` family over `fmt.Sprintf` on hot paths
      (fmt reflects and allocates); `fmt.Appendf` over Sprintf-then-copy.
      
      **string ↔ []byte**: each conversion copies. Avoid round-trips; work in one
      domain. Map lookups `m[string(bytes)]` and `switch string(b)` are
      compiler-optimized (no alloc) — don't contort code to avoid those. For
      zero-copy in extreme hot paths, 1.20+ `unsafe.String(ptr, len)` — only under
      the `rules/05 §7` unsafe policy, with the immutability invariant proven.
      
      **sync.Pool** — justified only when ALL hold: profile shows the allocation
      hot (GC pressure), objects are large or expensive, lifetime is strictly
      scoped (get → use → put, no retention), contents fully reset on put.
      Pools of small objects or pooling "just in case" adds complexity and can be
      slower. Classic correct use: per-request buffers in high-QPS encoders.
      
      ```go
      var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
      
      buf := bufPool.Get().(*bytes.Buffer)
      buf.Reset()                       // ALWAYS reset on get or put
      defer bufPool.Put(buf)
      ```
      
      Cap what you return to the pool (don't pool 64MB outliers — check
      `buf.Cap()` before Put). Never put slices whose backing array is still
      referenced elsewhere (aliasing corruption — HIGH).
      
      **Misc**: reuse buffers across loop iterations (`buf = buf[:0]`); avoid
      `[]byte(fmt.Sprintf(...))`; prefer streaming (`io.Copy`, `json.NewEncoder(w)`)
      over materializing (`io.ReadAll`, `json.Marshal` then `w.Write`); value
      receivers on small structs avoid pointer-chasing, but see escape analysis.
      
      ## 4. Escape analysis basics
      
      `go build -gcflags='-m'` (or `-m -m` for detail) shows what escapes to heap.
      You don't fight every escape — just know the triggers on hot paths:
      
      - Returning a pointer to a local → escapes (fine; that's how constructors work).
      - Storing into an interface (`fmt.Sprintf` args, `slog` `[]any`, `any` params)
        → escapes. This is why `LogAttrs`/`strconv.Append*` beat fmt on hot paths.
      - Closures capturing by reference, sending pointers to channels, slices that
        outgrow their scope → escape.
      - `make([]byte, n)` with variable `n` escapes; constant small `n` can stay on
        stack.
      - Don't return `*T` "for performance" on small structs — value returns often
        stay stack-allocated and copy cheaper than the GC cost of the escape.
      
      ## 5. GC tuning: GOGC and GOMEMLIMIT
      
      Defaults first; tune only with metrics (`runtime/metrics`, GC CPU fraction,
      RSS). Go 1.26 ships the Green Tea GC by default (10–40% lower GC overhead,
      more on AVX-512-class CPUs) — re-baseline GC metrics after upgrading before
      re-tuning; `GOEXPERIMENT=nogreenteagc` is a temporary escape hatch slated for
      removal in 1.27.
      
      - **`GOMEMLIMIT`** (1.19+) is the main knob in containers: set to ~90% of the
        container memory limit (`GOMEMLIMIT=900MiB` for a 1Gi pod). It's a soft
        limit — GC runs harder as you approach it instead of OOM-killing. Leave
        headroom for non-heap memory (stacks, cgo, mmap).
      - **`GOGC`** trades memory for CPU: `GOGC=200` halves GC frequency
        (more heap), `GOGC=50` doubles it (less heap). With GOMEMLIMIT set,
        `GOGC=off` + memlimit is a valid "use all the RAM I'm given" strategy for
        batch jobs; risky for spiky services (death-spiral near the limit —
        watch `/gc/limiter/last-enabled:gc-cycle`).
      - Symptoms → actions: high `runtime.mallocgc`/`gcBgMarkWorker` CPU → reduce
        allocations (§3) before touching knobs; OOMKilled pods → GOMEMLIMIT;
        RSS far below limit with GC CPU high → raise GOGC.
      - Ballast hacks are obsolete post-GOMEMLIMIT — audit and remove (LOW).
      
      ## 6. PGO builds
      
      Profile-guided optimization (default-on since 1.21 when a profile is present):
      put a representative CPU profile at `default.pgo` in the main package
      directory; `go build` picks it up automatically.
      
      ```bash
      curl -so cpu.pprof 'http://prod-host:6060/debug/pprof/profile?seconds=60'  # peak traffic
      mv cpu.pprof cmd/api/default.pgo
      go build ./cmd/api          # check logs: PGO enables extra inlining/devirtualization
      ```
      
      - Typical win 2–8% CPU; free once automated. SOTA setup: CI job refreshes
        `default.pgo` weekly from continuous-profiling data; stale profiles degrade
        gracefully (no correctness risk).
      - Verify it's active: `go build -pgo=auto -x` or check
        `go version -m binary` for the `-pgo` setting.
      
      ## 7. Production performance signals
      
      Profiles answer "where"; metrics answer "when/whether". Export from
      `runtime/metrics` (or via the OTel/Prometheus runtime collectors):
      
      - `/gc/heap/allocs:bytes` rate — allocation pressure trend; pair with
        `/cpu/classes/gc/total:cpu-seconds` (GC CPU share; >10–15% sustained means
        go to §3).
      - `/sched/latencies:seconds` — scheduler delay histogram; rising tail with
        idle CPU points at goroutine floods or syscall stalls (`go tool trace`).
      - `/memory/classes/heap/live:bytes` vs container limit — headroom for
        GOMEMLIMIT decisions; `/gc/limiter/last-enabled:gc-cycle` flags memlimit
        death-spiral mode.
      - `runtime.NumGoroutine()` as a gauge — monotonic growth is a leak
        (`rules/03 §2`), not a perf tuning problem.
      
      Alert on trends, not absolutes; baseline per service. Capture a CPU+heap
      profile automatically when alerts fire (flight recorder / continuous
      profiler) so the incident carries its own evidence.
      
      ## 8. Hot-path checklist (apply only where profiles point)
      
      - Bounds-check elimination: iterate with `for i := range s` / `for _, v`;
        hoist `_ = s[n-1]` hints rarely needed post-1.21.
      - Avoid defer in ultra-hot tiny functions pre-1.14 lore is obsolete — defer
        is ~1ns now; keep defers, drop the superstition (flag stale "defer is slow"
        comments as INFO).
      - Map with `int` keys beats `string` keys; consider sharded maps under
        write contention before `sync.Map`.
      - Sort with `slices.SortFunc` (no interface boxing) over `sort.Slice`.
      - JSON dominating? `json.NewDecoder`/`Encoder` streaming, smaller structs,
        `encoding/json/v2` (GOEXPERIMENT=jsonv2 — still experimental as of 1.26)
        or a faster codec — measure.
      
      ## Audit checklist
      
      ```bash
      # Profiling wired up? (absence in a latency-sensitive service = LOW gap)
      grep -rn 'net/http/pprof' --include='*.go' .
      grep -rn 'pprof' --include='*.go' . | grep -i 'listen\|mux\|handle'   # exposed publicly? — MEDIUM
      
      # Benchmarks exist for hot packages; b.Loop adoption
      grep -rln 'func Benchmark' --include='*_test.go' .
      grep -rn 'b.N' --include='*_test.go' .            # candidates to migrate to b.Loop (1.24+)
      grep -rn 'ReportAllocs' --include='*_test.go' .
      
      # Growth-by-append without prealloc near loops (then check if size was knowable)
      grep -rnE 'var \w+ \[\]' --include='*.go' . | head -50
      golangci-lint run --enable-only prealloc,perfsprint,makezero ./...
      
      # String concat in loops — O(n²)
      grep -rn -B3 '+= ' --include='*.go' . | grep -E 'for |range' | grep -i 'str\|msg\|out'
      
      # fmt on hot paths (handler/loop proximity — manual confirm)
      grep -rnE 'fmt\.Sprintf' --include='*.go' . | wc -l
      
      # sync.Pool correctness: Reset on reuse? cap check? aliasing?
      grep -rn -A5 'sync.Pool' --include='*.go' .
      
      # io.ReadAll on large/unbounded inputs — MEDIUM
      grep -rn 'io.ReadAll\|ioutil.ReadAll' --include='*.go' .
      
      # GC knobs & ballast
      grep -rn 'GOGC\|GOMEMLIMIT\|SetMemoryLimit\|SetGCPercent' -r . --include='*.go' --include='*.yaml' --include='Dockerfile*'
      grep -rn 'ballast' --include='*.go' .             # obsolete pattern — LOW
      
      # PGO
      ls cmd/*/default.pgo 2>/dev/null; grep -rn 'pgo' Makefile* .github/ 2>/dev/null
      
      # Escape analysis spot-check on hot package
      go build -gcflags='-m' ./internal/hotpkg 2>&1 | grep 'escapes to heap' | head -30
      ```
      
      Severity guide: pool aliasing/missing reset HIGH (corruption); unbounded
      ReadAll MEDIUM; missing prealloc/Builder on measured hot path LOW–MEDIUM;
      publicly exposed pprof MEDIUM; cargo-cult optimizations without benchmarks
      LOW.
      
    • 07-tooling-ci.md 12.8 KB
      # 07 — Tooling, testing, CI, go.mod hygiene
      
      A Go repo without lint+race+vuln gates accumulates every defect class in this
      skill silently. Tooling is cheap; retrofitting it is not. This file defines
      the SOTA gate set and test discipline.
      
      ## 1. CI gate set (minimum viable, in order)
      
      ```bash
      gofumpt -l -d .                          # formatting (superset of gofmt)
      go vet ./...
      golangci-lint run                        # curated config below
      go test -race -shuffle=on ./...          # shuffle kills inter-test ordering deps
      go test -race -coverprofile=cover.out ./...
      govulncheck ./...
      go mod tidy && git diff --exit-code go.mod go.sum
      go build ./...                           # catches main-package breakage tests miss
      ```
      
      Pin tool versions (see §4); cache `~/go/pkg/mod` and golangci-lint cache.
      A repo missing `-race` or govulncheck in CI: HIGH audit finding.
      
      On toolchain upgrades, run Go 1.26's revamped `go fix` — it now applies
      "modernizer" fixers (built on the vet analysis framework) that rewrite code
      to current idioms; review the diff like any refactor.
      
      ## 2. golangci-lint curated config
      
      Don't enable-all (noise kills adoption); don't run bare defaults (misses too
      much). Curated `.golangci.yml` (v2 schema):
      
      ```yaml
      version: "2"
      linters:
        default: none
        enable:
          # correctness
          - govet          # includes shadow-ish checks, lostcancel, copylocks
          - staticcheck    # the big one: SA bugs, ST style, S simplifications
          - errcheck       # unchecked errors
          - errorlint      # %w misuse, err == comparisons
          - nilerr         # return nil after err != nil
          - bodyclose      # unclosed http response bodies
          - rowserrcheck   # rows.Err() after iteration
          - sqlclosecheck  # rows/stmt Close
          - noctx          # http requests without context
          - contextcheck   # ctx not propagated
          - containedctx   # ctx stored in struct
          - copyloopvar    # obsolete loop-var copies (go >= 1.22)
          - gosec          # security incl. G115
          - musttag        # struct tags on (un)marshaled types
        settings:
          errcheck:
            check-type-assertions: true
          gosec:
            excludes: []        # triage per-finding with #nosec + justification
          staticcheck:
            checks: ["all"]
      formatters:
        enable: [gofumpt, goimports]
      ```
      
      Add per-repo: `prealloc`/`perfsprint` (perf-sensitive), `revive` (style
      depth), `exhaustive` (enum switches), `gomodguard` (dependency policy).
      Inline suppressions require a reason:
      `//nolint:gosec // G304: path validated by os.Root above` — bare `//nolint`
      is itself a LOW finding.
      
      `staticcheck` ships inside golangci-lint; running the standalone binary too
      is fine but redundant. `gofumpt` over `gofmt`: stricter, zero-config,
      no debates.
      
      Version notes (2026-06): golangci-lint v2.9.0+ is required for Go 1.26
      support (use the latest stable). `noctx` now also flags missing-ctx `log/slog`,
      `os/exec` and `crypto/tls` call sites, not just HTTP requests; `errcheck`
      v1.10+ excludes `crypto/rand.Read` by default (it never fails).
      
      ## 3. Test discipline
      
      **Table tests** are the default shape; name cases, use subtests:
      
      ```go
      func TestParseLevel(t *testing.T) {
          t.Parallel()
          tests := map[string]struct {
              in      string
              want    Level
              wantErr error
          }{
              "info":    {in: "info", want: LevelInfo},
              "unknown": {in: "nope", wantErr: ErrBadLevel},
          }
          for name, tt := range tests {
              t.Run(name, func(t *testing.T) {
                  t.Parallel()
                  got, err := ParseLevel(tt.in)
                  if tt.wantErr != nil {
                      if !errors.Is(err, tt.wantErr) {
                          t.Fatalf("err = %v, want %v", err, tt.wantErr)
                      }
                      return
                  }
                  if err != nil { t.Fatal(err) }
                  if got != tt.want { t.Errorf("got %v, want %v", got, tt.want) }
              })
          }
      }
      ```
      
      - **`t.Parallel()` correctness**: with `go.mod` ≥1.22 the loop-var capture
        trap is gone; on older modules every parallel subtest needs `tt := tt`.
        Parallel subtests + shared fixtures = races — fixtures must be per-subtest
        or immutable. `t.Setenv`/`t.Chdir` are incompatible with `t.Parallel`
        (panics — by design).
      - Use `t.Helper()` in assertion helpers, `t.Cleanup` over manual defers (runs
        even on Fatal, ordered LIFO, works with subtests), `t.TempDir()` for files,
        `t.Context()` (1.24+) for ctx.
      - Test behavior through exported APIs (`package foo_test`); reaching into
        internals couples tests to refactors. `export_test.go` for the rare
        internal hook.
      - Assertions: stdlib comparisons + `github.com/google/go-cmp` for deep diffs
        (`cmp.Diff(want, got)` in the error message). testify is acceptable if
        already in-house; don't mix styles.
      - **No time.Sleep synchronization** in tests — flaky by construction
        (MEDIUM). Use channels, fakes for clocks, or `testing/synctest` (1.25):
        `synctest.Test(t, func(t *testing.T){ ... })` runs goroutines in a bubble
        with virtual time — `time.Sleep` completes instantly and deterministically.
      
      **Integration tests — testcontainers** over mocks for DB/queue behavior:
      
      ```go
      func TestUserStore(t *testing.T) {
          if testing.Short() { t.Skip("integration") }
          ctx := t.Context()
          pg, err := postgres.Run(ctx, "postgres:17-alpine",
              postgres.WithDatabase("app"), postgres.BasicWaitStrategies())
          testcontainers.CleanupContainer(t, pg)
          if err != nil { t.Fatal(err) }
          // connect, migrate, exercise the real store
      }
      ```
      
      Gate with `testing.Short()` or build tags so `go test ./...` stays fast;
      CI runs both tiers. Mock at *your* consumer-side interfaces (hand-written
      fakes or `moq`/`mockgen` if codegen helps) — never mock `*sql.DB`.
      
      **Golden files** for large/structured outputs (rendered templates, JSON,
      codegen): store under `testdata/` (toolchain-ignored), compare bytes, update
      via flag:
      
      ```go
      var update = flag.Bool("update", false, "rewrite golden files")
      
      golden := filepath.Join("testdata", t.Name()+".golden")
      if *update { os.WriteFile(golden, got, 0o644) }
      want, _ := os.ReadFile(golden)
      if diff := cmp.Diff(string(want), string(got)); diff != "" {
          t.Errorf("mismatch (-want +got):\n%s", diff)
      }
      ```
      
      Review golden diffs like code — `-update` runs that get rubber-stamped make
      the tests decorative.
      
      **Fuzzing** (native, 1.18+) for every parser/decoder/validator that touches
      untrusted bytes:
      
      ```go
      func FuzzParseManifest(f *testing.F) {
          f.Add([]byte(`{"v":1}`))                  // seed corpus
          f.Fuzz(func(t *testing.T, data []byte) {
              m, err := ParseManifest(data)
              if err != nil { return }              // invalid input may error — must not panic
              round, err := m.MarshalBinary()       // invariants: roundtrip, no panic, bounded output
              if err != nil { t.Fatal(err) }
              _ = round
          })
      }
      ```
      
      `go test -fuzz=FuzzParseManifest -fuzztime=60s` in a periodic CI job; commit
      found crashers from `testdata/fuzz/` as permanent regression seeds. Seeds run
      on every normal `go test`.
      
      Coverage: track trend, don't worship a number; `-coverprofile` +
      `go tool cover -func` in CI. Untested error paths matter more than the
      percentage.
      
      ## 4. go.mod hygiene
      
      ```
      module github.com/org/app
      
      go 1.25.0          // language version (1.26's `go mod init` writes the previous minor by design)
      toolchain go1.26.5 // exact toolchain: reproducible builds across dev/CI (pin the current patch — verify at go.dev/doc/devel/release)
      
      require ( ... )
      
      tool (             // 1.24+: tool dependencies, versioned & sum-verified
          golang.org/x/tools/cmd/stringer
          github.com/sqlc-dev/sqlc/cmd/sqlc
      )
      ```
      
      - **`go` directive** is semantic, not decorative: it selects language
        behavior per-module (loop-var scoping needs ≥1.22 — `rules/03 §5`). Keep it
        within two releases of current (only the last two minors get security
        fixes — an EOL `go` directive with no newer toolchain is a MEDIUM finding).
      - **`toolchain` directive** pins the exact compiler; CI and developers build
        identically. Update deliberately (Dependabot/Renovate handle it).
      - **`tool` directives (1.24+)** replace the `tools.go` blank-import hack and
        ad-hoc `go install tool@version` drift: `go get -tool <pkg>`, run via
        `go tool stringer`. Tools become sum-verified supply chain (`rules/05 §8`).
        Audit repos still using floating `go install foo@latest` in CI: MEDIUM.
      - `go mod tidy` clean in CI (diff check); `go mod verify` on release builds.
      - Versioning: tag semver; v2+ requires the `/v2` module path suffix —
        retagging without it breaks consumers. Avoid `v0` forever for published
        libraries; commit to v1 once the API settles.
      - Workspaces (`go.work`) for local multi-module dev only — **never commit
        go.work to a library repo**; it's developer-machine state (`.gitignore` it).
      - `replace` directives in committed go.mod: temporary at best, document an
        expiry; they don't apply to downstream consumers of a library (so a library
        relying on `replace` is broken for users — HIGH).
      - **`require` is a floor, not a ceiling.** Under Minimal Version Selection the
        build uses the **highest** version required anywhere in the module graph, and a
        `require` line states a *minimum* — "required versions in go.mod files are
        minimum versions and may be increased automatically" (go.dev/ref/mod). So
        `require foo v1.2.3` — or a build-tool flag that becomes one, such as an
        `xcaddy --with foo@v1.2.3` — **cannot cap** `foo`: if anything else in the graph
        requires v1.5.0, you build v1.5.0 while your file reads v1.2.3. It is an exact
        pin only for a **leaf** dependency nothing else requires. Only `replace` caps:
        `exclude` drops a specific version and redirects that requirement to the *next
        higher* one, so it cannot hold a module back either.
      - Used deliberately, the floor is the right tool for a CVE fix — requiring the
        fixed version raises what gets selected without capping anything, and goes inert
        once upstream requires it anyway. But **say which you mean**: "we pinned it"
        reads as a ceiling to almost every reviewer, and for a transitive dependency it
        is not one. Verify what was actually built with `go list -m <module>` (or
        `go version -m ./bin/app`, §5) — never the `require` line you wrote. Same trap
        from the supply-chain side: `sota-devsecops` rules/03 §3.7.1.
      
      ## 5. Reproducible builds & release
      
      - Build with `-trimpath`; inject version via
        `-ldflags="-X main.version=$(git describe --tags)"` or read
        `debug.ReadBuildInfo()` (embeds VCS revision automatically).
      - `CGO_ENABLED=0` for static binaries unless cgo is required (`rules/05 §7`);
        distroless/scratch base images.
      - `go version -m ./bin/app` audits any binary's module versions and build
        settings — use it on artifacts you didn't build.
      
      ## Audit checklist
      
      ```bash
      # CI gates present? Inspect workflow files
      grep -rnE '(go test|race|govulncheck|golangci-lint|staticcheck|gofumpt)' .github/workflows/ Makefile* 2>/dev/null
      grep -rn 'test -race' . --include='*.yml' --include='*.yaml' --include='Makefile*' || echo 'NO RACE IN CI — HIGH'
      
      # Lint config exists and is curated (not empty, not enable-all)
      ls .golangci.yml .golangci.yaml 2>/dev/null
      grep -n 'enable-all\|disable-all' .golangci.y*ml 2>/dev/null
      
      # Suppression hygiene
      grep -rn 'nolint' --include='*.go' . | grep -v '//' | head            # malformed
      grep -rnE '//nolint(:\w+)?$' --include='*.go' .                       # no justification — LOW
      grep -rn '#nosec' --include='*.go' .                                  # justify each
      
      # go.mod hygiene
      grep -E '^(go|toolchain) ' go.mod         # version current? toolchain pinned?
      grep -A5 '^tool' go.mod                   # 1.24 tool directives in use?
      grep -rn 'tools.go' . 2>/dev/null         # legacy pattern — migrate (LOW)
      grep -rn 'go install .*@latest' .github/ Makefile* 2>/dev/null   # floating tools — MEDIUM
      git ls-files | grep 'go.work$' && echo 'go.work committed — check intent'
      grep -E '^replace' go.mod
      go list -m <module>                       # the SELECTED version — `require` is a floor, not a cap (§4)
      
      # Test quality
      grep -rln 'func Test' --include='*_test.go' . | wc -l
      grep -rn 't.Parallel' --include='*_test.go' . | wc -l
      grep -rn 'time.Sleep' --include='*_test.go' .                         # flaky sync — MEDIUM
      grep -rln 'func Fuzz' --include='*_test.go' .                         # parsers fuzzed?
      ls testdata/fuzz 2>/dev/null                                          # crash corpus committed?
      grep -rn 'testcontainers' go.mod
      go test -shuffle=on ./...                                             # ordering deps?
      go test -race -count=3 ./...
      
      # Formatting drift
      gofumpt -l . | head
      goimports -l . | head
      
      # Toolchain & vuln state
      go version; go env GOTOOLCHAIN
      govulncheck ./...
      ```
      
      Severity guide: no `-race`/govulncheck in CI HIGH; library shipping `replace`
      HIGH; EOL toolchain MEDIUM; floating tool versions MEDIUM; a `require`
      cited as a version *cap* for a non-leaf dependency MEDIUM (it is a floor — §4); sleep-synced or
      order-dependent tests MEDIUM; missing table tests / `t.Parallel` / golden
      review discipline LOW.
      
  • SKILL.md 8 KB
    ---
    name: sota-golang
    description: State-of-the-art Go engineering rules (2026 baseline, Go 1.25+) that Claude applies when writing new Go code or auditing existing Go code. Covers error handling, interface/package design, goroutine and channel correctness, net/http hardening, security (SQL, exec, path traversal, CSPRNG, TLS, supply chain), performance (pprof, allocations, GC, PGO), and tooling/CI. Trigger keywords - Go, golang, goroutine, channel, go.mod, errgroup, context.Context, pprof, govulncheck, net/http, slog. Use for BOTH building Go services/libraries/CLIs and reviewing or auditing Go codebases.
    ---
    
    # SOTA Go (2026)
    
    Expert-level rules for producing and auditing production Go. Baseline language
    version: Go 1.25+, the oldest release still in security support (Go fixes the
    last two majors; 1.24 left support with 1.26's release, 2026-02). Feature
    notes: loop-var scoping from 1.22, `b.Loop`/`os.Root`/tool directives from
    1.24, `testing/synctest` and container-aware GOMAXPROCS from 1.25,
    `errors.AsType` and the default-on Green Tea GC from 1.26 — noted where
    relevant. Every rule states the *why*; every rules file
    ends with an audit checklist of grep/vet/lint patterns.
    
    ## Purpose
    
    Two consumers, one source of truth:
    
    - **BUILD mode** — generating new Go code: follow the rules as defaults, not
      suggestions. Deviate only with an explicit comment justifying it.
    - **AUDIT mode** — reviewing existing Go code: hunt violations using the audit
      checklists, classify by severity, report in the finding format below.
    
    ## BUILD mode
    
    1. Before writing code, read the rules files relevant to the task (see index).
       A service touching HTTP + DB + goroutines needs `03`, `04`, `05`.
    2. Apply the **top-10 non-negotiables** (below) unconditionally.
    3. New modules: `go mod init` with a real module path; since 1.26 it writes
       the previous minor as the `go` directive (e.g. `go 1.25.0`) for ecosystem
       compatibility — keep that unless you need newer language features; pin the
       `toolchain` directive to the current patch release. Add `golangci-lint`
       config and a CI step
       running `go vet`, `golangci-lint run`, `go test -race ./...`,
       `govulncheck ./...` from day one (see `rules/07`).
    4. Prefer stdlib. Each dependency must earn its place (see `rules/05` supply
       chain section).
    5. Write table tests alongside the code, not after. Exported behavior gets a
       test; concurrency gets a `-race` test; parsers get a fuzz target.
    6. When generating code that violates a rule for a legitimate reason (e.g.
       `sync.Pool` complexity, `unsafe`), leave a `// NOTE(sota):` comment
       explaining the trade-off so auditors don't flag it blind.
    
    ## AUDIT mode
    
    Work through each relevant rules file's audit checklist against the target
    repo. Run the listed grep/vet/lint commands; confirm each hit manually before
    reporting (greps are recall-oriented, expect false positives).
    
    ### Severity conventions
    
    | Severity | Meaning | Examples |
    |---|---|---|
    | **CRITICAL** | Exploitable or guaranteed-incorrect in production | SQL built with `fmt.Sprintf`, command injection via `sh -c`, unbounded goroutine leak on hot path, `InsecureSkipVerify: true`, data race confirmed by `-race` |
    | **HIGH** | Likely production incident or security weakness | Missing `http.Server` timeouts, no ctx cancellation on blocking goroutine, unchecked integer truncation on attacker input (G115), `resp.Body` never closed, panic for control flow in a server |
    | **MEDIUM** | Correctness/maintainability hazard, latent bug | Error strings compared with `strings.Contains`, context stored in struct, `time.After` in a loop, map writes without lock under suspected concurrency, missing `errors.Is/As` |
    | **LOW** | Idiom/perf debt, works but wrong shape | Returning interfaces, `util` package dumps, missing preallocation on hot path, non-table tests, no `t.Parallel` |
    | **INFO** | Style, doc, or hygiene note | Naming, missing doc comments, gofumpt drift |
    
    ### Finding format
    
    ```
    [SEVERITY] file.go:LINE — short title
      Rule: rules/NN-name.md § section
      Evidence: the offending line(s), verbatim
      Impact: one sentence — what goes wrong, under what conditions
      Fix: concrete replacement code or action
      Effort: trivial | small | medium | large
    ```
    
    Group findings by severity, CRITICAL first. End the audit with: counts per
    severity, the three highest-leverage fixes, and which checklists were run.
    
    ## Rules index
    
    | File | Read this when... |
    |---|---|
    | `rules/01-errors.md` | Writing/reviewing any error path: wrapping with `%w`, `errors.Is/As`, sentinel vs typed errors, **in-band sentinels (absence encoded as `-1`/`0`/`""`)** and comma-ok, panic/recover policy, error API design for libraries vs apps |
    | `rules/02-design.md` | Designing packages or APIs: interface placement and size, package layout and `internal/`, naming, zero values, generics restraint, embedding, functional options, `context.Context` discipline |
    | `rules/03-concurrency.md` | Anything with `go`, `chan`, `sync`, or `select`: goroutine lifecycle ownership, leak catalog, errgroup fan-out, channels-vs-mutex decision, race patterns, worker pools, semaphores, `time.After` traps |
    | `rules/04-http-services.md` | Building or auditing HTTP servers/clients: all five server timeouts, client timeouts and body hygiene, connection reuse, graceful shutdown, middleware, `slog` structured logging, request-scoped values |
    | `rules/05-security.md` | Any input crossing a trust boundary: SQL parameterization, `os/exec` safety, path traversal and `os.Root`, integer overflow (G115), output encoding (`html/template`), CSPRNG (`crypto/rand` vs `math/rand`), TLS config, `unsafe`/cgo policy, govulncheck, supply chain and go.sum |
    | `rules/06-performance.md` | Latency/memory work: pprof workflow, `testing.B` + `b.Loop`, allocation reduction, `strings.Builder`, `sync.Pool` criteria, escape analysis, GOGC/GOMEMLIMIT, PGO |
    | `rules/07-tooling-ci.md` | Setting up or auditing CI and tests: golangci-lint curated config, staticcheck/gofumpt/vet, table tests, `t.Parallel` correctness, testcontainers, golden files, fuzzing, go.mod hygiene and `tool` directives. **Test *strategy* — suite shape, TDD, doubles, test data, flake policy — lives in `sota-testing`; load it for any build that writes logic. This file owns Go runner mechanics only.** |
    
    ## Top-10 non-negotiables
    
    1. **Every error is handled or wrapped with `%w` and context** — never
       discarded with `_`, never logged-and-ignored on a path that must abort.
       Compare with `errors.Is`/`errors.As`, never string matching. (`rules/01`)
    2. **No panics for control flow.** `panic` is for unreachable programmer
       errors only; servers recover at goroutine boundaries and log. (`rules/01`)
    3. **Every goroutine has an owner and a guaranteed exit path** — tied to a
       `context.Context`, a closed channel, or a `WaitGroup`/`errgroup` join. If
       you can't say how it stops, don't start it. (`rules/03`)
    4. **`go test -race ./...` in CI, always.** A race detector failure is a
       CRITICAL finding, not flaky-test noise. (`rules/03`, `rules/07`)
    5. **`http.Server` sets `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout`,
       `IdleTimeout`; clients set timeouts and `defer resp.Body.Close()` with
       drain.** Default zero timeouts are a DoS. (`rules/04`)
    6. **SQL only via parameterized queries** (`database/sql` placeholders, pgx,
       or sqlc-generated code). String-built SQL is CRITICAL, no exceptions for
       "internal" values. (`rules/05`)
    7. **`os/exec` with argv lists, never `sh -c` with interpolated input;
       file paths validated against a root** (`os.Root` on 1.24+, else
       `filepath.Clean` + prefix check after resolving symlinks). (`rules/05`)
    8. **`context.Context` is the first parameter, flows down, is never stored in
       a struct**, and carries only request-scoped metadata — never dependencies.
       (`rules/02`)
    9. **Accept interfaces, return structs; define interfaces at the consumer,
       keep them small.** No premature interfaces "for mocking". (`rules/02`)
    10. **`govulncheck ./...` and `golangci-lint` gate CI**; `go.sum` committed;
        dependencies minimal and justified. (`rules/05`, `rules/07`)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related