Skip to content

Go Service Standards

FieldValue
TypeSkill Resource
Source~/.copilot/skills/backend/references/go-standards.md
DescriptionNot specified

Source Content

Go Service Standards

This document consolidates the house standards for writing Go services on the P3 stack: Fiber HTTP, pgx/GORM, Zap, OpenTelemetry, Prometheus, and oapi-codegen from api/openapi.yaml. Every new Go service should follow these rules, and every code review should measure against them.

When a rule here and an agent body disagree, this file and the ADRs it cites (ADR-024, ADR-027, ADR-023) win. See the parent SKILL.md for the complete routing table and how to navigate this reference.

Quick reference: The five foundations

  1. Layering: handler → service → repository → domain (dependencies point inward only).
  2. Error handling: wrap every boundary error with %w + context; map to HTTP status only in the handler.
  3. Concurrency: no raw go keyword — route through sourcegraph/conc; every blocking call takes context.Context.
  4. Testing: table-driven by default; ≥80% coverage on domain/service; -race non-negotiable.
  5. Observability: wired from the first commit — Zap with trace correlation, OTel spans, Prometheus metrics, pprof.

Layering: handler → service → repository → domain

See layering.md for the full architecture, import-direction rules, and a worked example. The key insight: repository is an interface defined in the service layer and implemented in the repository layer; the service depends on the interface, not the concrete type. Wire concrete types only in main.

Error handling

Full patterns and the handler error-mapping table: see errors.md. Key rules:

  • Wrap at every boundary with %w and context: fmt.Errorf("load invoice %s: %w", id, err). The reader reconstructs the call path from the message chain.
  • Sentinel errors for expected conditions (checked with errors.Is); typed errors when callers need fields (extracted with errors.As).
  • Domain errors live in domain; the handler is the only layer that maps them to HTTP status (RFC 9457 Problem Details — ADR-027).
  • Never panic for control flow; panic only on programmer error at startup. Recover middleware is the safety net, not the strategy.
  • Never discard an error with _ = unless you write the one-line reason why it is safe.

Concurrency

See concurrency.md for worked conc patterns, context-propagation rules, and the -race workflow.

Key rules:

  • No raw go keyword in service code. Use sourcegraph/concconc.WaitGroup for fire-and-forget, pool.ResultErrorPool for fan-out-with-results, stream for ordered output. It recovers panics and propagates them instead of crashing.
  • Every blocking call takes context.Context as its first parameter and honors cancellation.
  • -race is non-negotiable — task test runs go test -race ./... every time. A data race is a failed build.

Testing

See testing.md for the table-test template, coverage commands, and the testcontainers integration pattern.

Key rules:

  • Table-driven by default. One []struct of cases, one t.Run(tc.name, ...) loop. Add t.Parallel() when cases are independent.
  • Coverage floor: ≥80% on domain and service. Handlers and repositories are covered by integration tests, not chased for line coverage.
  • TDD cycle (ADR-024): RED → GREEN → REFACTOR. Write the failing test first.
  • Mock at the interface, not the struct. The service tests against a fake repository implementing the interface; no DB needed for unit tests.
  • task test-cover enforces the floor and fails under threshold.

Observability

See observability.md for logger construction, trace-correlation core, and metrics middleware.

Not a later milestone. The first handler ships with logging, tracing, and metrics already correlated.

  • Zap for structured JSON logs, with trace_id and span_id injected from the active OTel span on every entry. Wide events: one log line per operation, static grep-stable msg, dynamic values in fields (ADR-023). Never log the never-log list (ADR-025).
  • OpenTelemetry tracing via otelfiber middleware; context propagates through service and repository so the trace is one unbroken span tree.
  • Prometheus /metrics endpoint; RED metrics (Rate, Errors, Duration) on every handler via middleware.
  • pprof mounted on a separate internal port, gated by DEBUG=true — never exposed on the public listener in prod.
  • Middleware order is load-bearing: recover → otel → zap → metrics → auth → routes.

OpenAPI as the source of truth

See openapi.md for codegen config, the implement-the-interface pattern, and the drift check.

  • The spec at api/openapi.yaml is authoritative. Code is generated from it, never the reverse.
  • oapi-codegen generates server interface + types; you implement the generated interface. Generated code is committed so diffs are reviewable and CI is hermetic.
  • Regeneration is a build step (task openapi), and CI fails if generated code drifts (git diff --exit-code after regen).

Database (Postgres via pgx/GORM)

See database.md for transaction/isolation patterns, pagination, scanning, bulk insert, and the three-layer test approach.

Key rules:

  • defer tx.Rollback(ctx) immediately after Begin(ctx) — a no-op after a successful commit, the only reliable cleanup on every early-return error path.
  • Pick the isolation level and row lock deliberately — Read Committed by default, SELECT ... FOR UPDATE when a transaction must own a row through its lifetime.
  • Retry the whole transaction on Postgres error 40001 (serialization failure), never just the failing statement.
  • Cursor (keyset) pagination over OFFSETOFFSET still scans and discards every skipped row; a cursor with an index does not.
  • pgx.CollectRows + RowToStructByName for scanning; pointer fields over sql.NullString/sql.NullInt64 for nullable columns.
  • pgx.ErrNoRows maps to domain.ErrNotFound at the repository boundary — the same sentinel-error pattern.

Security

See security.md for the severity-tagged checklist, filesystem/JWT/cookie/crypto patterns.

Coding-time checklist:

  • Validate at the boundary — the handler is where untrusted input gets checked; everything inward trusts its callers.
  • os.Root (Go 1.24+) for any user-supplied file path — the primary defense against path traversal; validate every archive-entry path against the root before extracting (zip-slip).
  • Pin the JWT signing algorithm explicitly — never trust the alg header from an attacker-controlled token. Argon2id with OWASP parameters for password hashing, never bcrypt-with-defaults.
  • __Host-/__Secure- cookie prefixes on any session cookie, with HttpOnly, Secure, and SameSite set.
  • Never echo internal error text to a client (the existing 500-mapping rule in errors.md) — this is the same boundary discipline applied to security.
  • pprof stays loopback-only or auth-gated, even behind DEBUG=true — hardens the existing observability rule.

Safety

See safety.md for the complete gotcha list with before/after code.

Defensive-coding gotchas:

  • append can silently overwrite a shared backing array when the target slice still has spare capacity — force reallocation (a[:len(a):len(a)] or slices.Clone) before appending to a slice that might be shared.
  • A small subslice keeps its entire backing array alive — clone before discarding the large source.
  • defer in a loop accumulates until the function returns, not the iteration — wrap the loop body in its own function when deferring per-item cleanup.
  • The typed-nil-in-interface trap: var err *MyError; return err is a non-nil error. Return a literal nil.
  • Concurrent map read/write is a fatal, unrecoverable crash — reinforcing the concurrency rule (“shared state needs a guard”).
  • Never copy a sync type by value, including via a value-receiver method — go vet won’t always catch it.

Performance

See performance.md for the profile-first workflow and CI benchmark-gate example.

Key rule: Baseline → pprof → one change → benchstat → evidence in the PR. Never guess-and-change.

  • GOMEMLIMIT at 80–90% of the container limit on k8s; Go 1.25+ reads cgroup v2 quotas for GOMAXPROCS automatically.
  • CI benchmark gates compare against a baseline with benchstat, never an absolute threshold — cloud CI has 5–10% run-to-run noise.

Lint

See lint.md for the golangci-lint set beyond the defaults and what each linter catches.

The linter set: errorlint · nilerr · forcetypeassert · containedctx · fatcontext · bodyclose · sqlclosecheck · rowserrcheck · durationcheck · copyloopvar · paralleltest · nolintlint. Each one has caught a real bug class before.

Dependencies

See dependencies.md for dependency-liability framing, the tool directive, and CI auto-merge policy.

Key rules:

  • Ask before adding a new third-party dependency; upgrading an existing one needs no check-in.
  • go.mod’s tool directive (Go 1.24+) replaces the old tools.go blank-import hack for pinning build tools like oapi-codegen.
  • govulncheck in CI narrows to reachable call paths — don’t over-react to a CVE it stays silent on.
  • go.work for local multi-module dev only — never commit go.work.sum; strip replace directives before a release.

Terminal UIs (TUI)

See tui.md for library comparison, the Elm-architecture pattern, async Cmd wiring, and teatest examples.

Key rules:

  • Bubbletea + Bubbles + Lipgloss is the default stack. Reach for tview/tcell only for a stated reason.
  • The model stays thin. Presentation state (cursor, focus, scroll) lives in the tea.Model; business rules live in plain Go types with no bubbletea import, built and tested the same as any domain/service code.
  • No raw go inside a tea.Cmd. A Cmd already runs on its own goroutine; fan-out inside it still goes through sourcegraph/conc.
  • Style through Lipgloss, never raw ANSI. Define a small style set once; don’t hand-write escape sequences.
  • Test the domain logic directly (table-driven, no bubbletea import) and smoke-test the Update/View wiring with teatest.

How to use this reference

  • Pick your task from the SKILL.md routing table.
  • Read the relevant section above.
  • Dive into the linked reference file (e.g., errors.md, testing.md) for patterns and examples.
  • Run scripts/lint_go.sh from the module root to verify your code against this standard.