For the Go backend — serving · worker (ingest/detectors) · MCP · store. Base:
Effective Go + the Google Go Style Guide; format + lint with golangci-lint v2
(go tool golangci-lint fmt / run — config .golangci.yml). This file is the
project-specific delta. Versions, the tool directive, and the Go-1.26 feature adoption
live in TOOLCHAIN.md.
- Go 1.26 (pinned via
go.modtoolchain, TOOLCHAIN §1). Module:danny.vn/mise. - Lean on the runtime, don't fight it: container-aware
GOMAXPROCS(don't override it on GKE), Green Tea GC default,os.Rootfor file sandboxing,new(expr)for nullable metadata pointers — the full adoption list is TOOLCHAIN.md §3.
cmd/<binary>/main.gois thin — parse config, wire dependencies, start. Logic lives inpkg/.- Small interfaces, defined by the consumer; accept interfaces, return concrete structs.
- Functional options for configurable constructors:
NewClient(opts ...Option)wheretype Option func(*Client)— proven in s1ctl for SDK clients; use foringest.Source, embedder, MCP clients, store constructors. context.Contextis the first parameter of anything doing I/O; honour cancellation.- Every Vertex touchpoint sits behind a Go interface (embed · parse · judge · ground) so it can be faked offline (LOCAL-DEV §4) — never hard-wire the SDK call.
- Auth as
http.RoundTripper: wrap auth (OIDC token injection, ADC) as a transport decorator — composable, testable, keeps auth out of business logic. - REST handlers (huma v2, first use in
pkg/httpapi): one typedInput/Outputstruct pair per operation — path/query params via struct tags (path:"ref",query:"max_depth"), the payload underOutput.Body. Wire types (*Wiresuffix, e.g.EdgeWire) map 1:1 to the domain type they mirror and are never the domain type itself, so a schema change is a deliberate, visible edit. Errors arehuma.ErrorNNNxxx(msg, err)(RFC 9457application/problem+json) for an expected 4xx; return a plain wrappederroronly for a genuine failure — huma maps that to a 500 itself.
- Wrap with
fmt.Errorf("doing X: %w", err); classify witherrors.Is/errors.As; define sentinel errors for known conditions. - Typed errors for external boundaries: define
*APIError(Vertex),*StoreError(AlloyDB),*IngestError(source connectors) with structured fields (status, code, message) — test witherrors.As, not string matching. - No
panicin library code. Return errors; log them once, at the boundary (don't log-and-return).
- sqlc for type-safe queries. Hand-written SQL only for the retrieval CTEs the banhmi/laksa engine already maintains (hybrid ScaNN + BM25 / RRF) — keep them in one place, commented.
- RLS is enforced in the DB; bind the caller's tier to the session role per request.
- Always thread
context; never leak goroutines; bound worker pools. Temporal owns ingest fan-out — don't reinvent retry/orchestration in app code.
log/slog, structured JSON; carryrun_id/ request id. Nofmt.Printlnin services.
- Table-driven tests with
t.Run; fakes via the Vertex interfaces; run with-racein CI. testing/synctest(Go 1.25) for concurrent / timeout / backoff logic in workers and Temporal activities — virtual time, deterministic. Full strategy: TESTING.md.
- Packages: short, lowercase, no underscores or plurals. Document every exported identifier
(
reviveexported+var-namingenforce MixedCaps / initialisms). - Filenames: lowercase, words run together or
snake_caseonly where it aids reading (hybrid_search.go);_test.gofor tests,_<GOOS>.gofor platform files. One concern per file; the file name names the concern.
Enforced by .golangci.yml, not just asked for:
- File length ≤ ~500 lines (
revivefile-length-limit) — split a growing file by concern before it sprawls. - Function length ≤ 80 lines / 50 statements (
funlen); line length ≤ 120 (lll); cognitive complexity ≤ 30 (revive). - Go has no official hard line limit (Google Go Style) — 120 is our cap for readability.
go tool golangci-lint fmtauto-formats;run --fixauto-applies fixable findings.