Go Service Standards
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/backend/references/go-standards.md |
| Description | Not 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
- Layering: handler → service → repository → domain (dependencies point inward only).
- Error handling: wrap every boundary error with
%w+ context; map to HTTP status only in the handler. - Concurrency: no raw
gokeyword — route throughsourcegraph/conc; every blocking call takescontext.Context. - Testing: table-driven by default; ≥80% coverage on domain/service;
-racenon-negotiable. - 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
%wand 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 witherrors.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
panicfor 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
gokeyword in service code. Usesourcegraph/conc—conc.WaitGroupfor fire-and-forget,pool.ResultErrorPoolfor fan-out-with-results,streamfor ordered output. It recovers panics and propagates them instead of crashing. - Every blocking call takes
context.Contextas its first parameter and honors cancellation. -raceis non-negotiable —task testrunsgo 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
[]structof cases, onet.Run(tc.name, ...)loop. Addt.Parallel()when cases are independent. - Coverage floor: ≥80% on
domainandservice. 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-coverenforces 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_idandspan_idinjected from the active OTel span on every entry. Wide events: one log line per operation, static grep-stablemsg, dynamic values in fields (ADR-023). Never log the never-log list (ADR-025). - OpenTelemetry tracing via
otelfibermiddleware; context propagates through service and repository so the trace is one unbroken span tree. - Prometheus
/metricsendpoint; 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.yamlis authoritative. Code is generated from it, never the reverse. oapi-codegengenerates 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-codeafter 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 afterBegin(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 UPDATEwhen 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
OFFSET—OFFSETstill scans and discards every skipped row; a cursor with an index does not. pgx.CollectRows+RowToStructByNamefor scanning; pointer fields oversql.NullString/sql.NullInt64for nullable columns.pgx.ErrNoRowsmaps todomain.ErrNotFoundat 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
algheader from an attacker-controlled token. Argon2id with OWASP parameters for password hashing, never bcrypt-with-defaults. __Host-/__Secure-cookie prefixes on any session cookie, withHttpOnly,Secure, andSameSiteset.- 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:
appendcan silently overwrite a shared backing array when the target slice still has spare capacity — force reallocation (a[:len(a):len(a)]orslices.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.
deferin 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 erris a non-nilerror. Return a literalnil. - Concurrent map read/write is a fatal, unrecoverable crash — reinforcing the concurrency rule (“shared state needs a guard”).
- Never copy a
synctype by value, including via a value-receiver method —go vetwon’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.
GOMEMLIMITat 80–90% of the container limit on k8s; Go 1.25+ reads cgroup v2 quotas forGOMAXPROCSautomatically.- 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’stooldirective (Go 1.24+) replaces the oldtools.goblank-import hack for pinning build tools likeoapi-codegen.govulncheckin CI narrows to reachable call paths — don’t over-react to a CVE it stays silent on.go.workfor local multi-module dev only — never commitgo.work.sum; stripreplacedirectives 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/tcellonly 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 nobubbleteaimport, built and tested the same as any domain/service code. - No raw
goinside atea.Cmd. ACmdalready runs on its own goroutine; fan-out inside it still goes throughsourcegraph/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
bubbleteaimport) and smoke-test theUpdate/Viewwiring withteatest.
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.shfrom the module root to verify your code against this standard.