Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Language · Go 1.26 · stdlib net/http · database/sql + pgx/v5 · golangci-lint v2.12.2

Go

Idiomatic Go — small interfaces, explicit errors, standard library over frameworks.

gogolangbackendstdlibconcurrency

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You write idiomatic, stdlib-first Go for the current stable toolchain. "Good" here means: small composable packages, errors wrapped and inspected with errors.Is/As, context threaded through every blocking call, no goroutine leaks, zero go vet/golangci-lint findings, and table-driven tests that pass with -race. Reach for a framework only when the stdlib genuinely falls short.

Stack

  • Go 1.26.4 (current stable). Set go 1.26 in go.mod and pin the toolchain with toolchain go1.26.4. Use Go modules only — no GOPATH layout, no dep/glide.
  • Formatting/vetting: gofmt (non-negotiable), go vet ./..., and golangci-lint v2 (golangci-lint run). Pin the exact patch in CI (currently v2.12.2) — a floating v2 drifts lint results between runs. Format with golangci-lint fmt (gofmt + goimports + gofumpt live under the v2 formatters: block, not linters-settings:).
  • Modernization: go fix ./... — rewritten in 1.26 on the analysis framework, it applies ~two dozen "modernizers" (e.g. min/max, slices.Contains, ranged ints). Honor //go:fix inline directives to migrate off deprecated constants/wrappers.
  • HTTP: stdlib net/http with the 1.22+ method+pattern ServeMux. Add chi v5 only for large routers needing sub-routers/middleware groups. Avoid Gin/Echo/Fiber unless the team already standardized on one.
  • DB: database/sql + jackc/pgx/v5 (use pgx native pool for Postgres-heavy code; the stdlib adapter when you need database/sql). Migrations via pressly/goose or golang-migrate. No heavyweight ORM; prefer sqlc to generate typed queries from SQL over GORM.
  • Logging: stdlib log/slog (structured). No logrus/zap in new code unless perf-profiled to need it.
  • Tooling deps: track with the tool directive in go.mod (go get -tool <pkg>, run via go tool <name>). Delete any legacy tools.go build-tag file.
  • Config/CLI: stdlib flag for simple binaries; spf13/cobra for multi-command CLIs. Env via os.Getenv or caarlos0/env, not a global singleton.

Project conventions

  • Layout: cmd/<binary>/main.go per executable; importable code under internal/ (compiler-enforced privacy) or the module root; no pkg/ cargo-cult directory. Keep main thin — parse flags, wire deps, call into packages.
  • Package names: short, lowercase, no underscores, no plurals, singular noun (user, not users/userutils). Ban grab-bag packages util, common, helpers, base, misc, shared.
  • No stutter: the package qualifies the name. Export user.Store, not user.UserStore; http.Server, not http.HTTPServer.
  • One type's methods live in one file named after the type (server.go, store.go). Test files sit beside them (server_test.go).
  • Imports in three gofmt-sorted groups (stdlib / third-party / local) — let goimports/golangci-lint fmt enforce it. The local module path is the import prefix.
  • Exported identifiers get doc comments starting with the identifier name (// Server handles ...). go vet and revive check this.
  • Constructors return the concrete type: func New(...) *Server. Return error as the last value.

Errors

  • Wrap with context when crossing a boundary: fmt.Errorf("load config %s: %w", path, err). Lowercase, no trailing punctuation, no "failed to" prefix (the chain already reads as a failure).
  • Wrap once per boundary, not at every call site — over-wrapping produces a: b: c: d: real error noise. Add %w only where it adds locating information.
  • Inspect with errors.Is(err, ErrNotFound) (sentinel) and errors.As(err, &target) (typed). Never compare with == across wrapped errors or strings.Contains(err.Error(), ...).
  • Sentinels: var ErrNotFound = errors.New("not found") at package scope. Custom types implement Error() string and, when wrapping, Unwrap() error.
  • Aggregate independent failures with errors.Join(err1, err2) (both stay discoverable via Is/As).
  • Never discard errerrcheck flags _ = f(). Handle, return, or log; if truly ignorable, comment why. Always check defer closers on writers: defer func() { err = errors.Join(err, f.Close()) }() for files you wrote to.
  • No panic in library code. Panic only for genuinely unreachable invariants (panic("unreachable")); recover only at goroutine/request boundaries and convert to an error. Return errors, don't log.Fatal outside main.
  • Don't log-and-return the same error — pick one. Log at the top (handler/main), return everywhere below.

Interfaces

  • Accept interfaces, return concrete structs. Functions take the smallest interface they use; constructors hand back *Thing.
  • Define interfaces at the consumer, not next to the implementation. The package that calls Save(ctx, u) declares type store interface { Save(...) error }, usually unexported.
  • Keep them tiny — 1–3 methods. Prefer stdlib shapes (io.Reader, io.Writer, fmt.Stringer) over bespoke ones.
  • Use any, never the alias interface{}. Reach for generics ([T any], [T cmp.Ordered]) instead of any + type assertions when the type is uniform.
  • Don't define an interface with a single implementation "for testing" pre-emptively — introduce it when a second caller/mock actually needs it.
  • Guard intended implementations at compile time: var _ Store = (*PostgresStore)(nil).

Concurrency

  • context.Context is the first parameter of every blocking/IO function: func (s *Server) Fetch(ctx context.Context, id string) (...). Never store a context in a struct; never pass nil — use context.TODO() only as a temporary marker.
  • Every goroutine needs a defined exit. A goroutine writing to an unbuffered channel with no reader on the cancel path is a leak — select on ctx.Done() or size the buffer.
  • Prefer golang.org/x/sync/errgroup for fan-out with first-error + cancellation. Use g.SetLimit(n) to bound concurrency:
    g, ctx := errgroup.WithContext(ctx)
    for _, u := range urls {
        g.Go(func() error { return fetch(ctx, u) }) // 1.22+: no u := u needed
    }
    if err := g.Wait(); err != nil { return err }
    
  • Plain wait groups: use wg.Go(func(){ ... }) (added Go 1.25) instead of manual Add(1)/defer Done().
  • Channels to hand off ownership/stream; sync.Mutex to guard shared state. Don't model a simple shared counter with a channel. Keep the mutex next to the field it protects, keep critical sections short, and never call out to unknown code while holding a lock.
  • Use sync.OnceValue/sync.OnceFunc for lazy init instead of a hand-rolled once.Do + package var.
  • Timeouts/cancellation: ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel(). Always defer cancel() even on the success path.
  • Graceful shutdown in main: ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM); defer stop(), then srv.Shutdown(ctx).
  • Run tests and CI with -race. Guard time-dependent concurrency tests with testing/synctest (stable in 1.25) rather than time.Sleep.

Prefer stdlib

  • Routing (1.22+): register method+path patterns on http.ServeMux; read params with r.PathValue:
    mux := http.NewServeMux()
    mux.HandleFunc("GET /items/{id}", h.get)
    mux.HandleFunc("POST /items", h.create)
    id := r.PathValue("id")
    
  • Never call http.ListenAndServe bare. Build an http.Server{Handler, ReadHeaderTimeout: 5*time.Second, ReadTimeout, WriteTimeout, IdleTimeout} — an unset ReadHeaderTimeout is a Slowloris DoS. Set BaseContext to your shutdown context.
  • HTTP clients: never use http.DefaultClient for external calls with no timeout. Construct &http.Client{Timeout: ...} and drive per-request deadlines with http.NewRequestWithContext.
  • DB: always the context methods — QueryContext, ExecContext, QueryRowContext, BeginTx. Parameterize ($1/?); never fmt.Sprintf values into SQL. Configure the pool: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime. defer rows.Close() and check rows.Err() after the loop.
  • JSON: encoding/json. Use json:",omitzero" (Go 1.24) for clean optional-field omission — it fires on the true zero value / IsZero(), fixing omitempty's inability to omit zero structs and time.Time. Decode with json.NewDecoder(r.Body) and call dec.DisallowUnknownFields() for strict APIs. encoding/json/v2 exists but is still GOEXPERIMENT=jsonv2 — don't ship it in production yet.

Modern stdlib (use these, not hand-rolled)

  • Builtins: min, max, clear (maps/slices) — Go 1.21. new(expr) for pointer-to-value — Go 1.26.
  • slices: Contains, Index, Sort, SortFunc, BinarySearch, Equal, Clone, Compact, Delete, Insert, Max/Min. maps: Keys, Values (return iter.Seq), Clone, Equal. cmp: Compare, Or, Less.
  • Iterators (Go 1.23): range over iter.Seq[T]/iter.Seq2[K,V]. Consume maps.Keys(m) directly with for k := range maps.Keys(m); use slices.Collect/maps.Collect to materialize. Prefer strings.Lines/strings.SplitSeq/strings.FieldsSeq (1.24) over allocating Split.
  • for i := range n {} to loop a fixed count (Go 1.22). Loop variables are per-iteration since 1.22 — delete every x := x shadow copy.
  • log/slog for logs: slog.InfoContext(ctx, "msg", "key", val); set a JSON handler in main.
  • time: prefer time.Since/time.Until. Compare instants with .Equal, not ==.

Testing

  • go test ./... -race -shuffle=on. Standard library testing first; add google/go-cmp for deep struct diffs (cmp.Diff(want, got)) and testify/require only where its assertions materially cut noise — don't pull a BDD framework.
  • Table-driven tests, subtests via t.Run(tc.name, ...), and t.Parallel() in both the outer test and each subtest when the code is parallel-safe. Loop vars are safe to capture (1.22+).
    tests := []struct{ name, in, want string }{ ... }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()
            got := Slug(tc.in)
            if got != tc.want {
                t.Errorf("Slug(%q) = %q, want %q", tc.in, got, tc.want)
            }
        })
    }
    
  • Use t.Context() (Go 1.24) for a test-scoped, auto-cancelled context and t.Cleanup(...) for teardown instead of trailing defers. t.TempDir() for filesystem tests.
  • Test behavior through exported APIs; keep tests in package foo_test (black-box) unless you must reach internals. Assert error identity with errors.Is, not string matching.
  • Benchmarks: use for b.Loop() {} (Go 1.24) — it scopes the timer and stops the body being optimized away; drop the old for i := 0; i < b.N; i++ form.
  • Add fuzz tests (func FuzzX(f *testing.F)) for parsers/decoders and any []byte/string boundary. Deterministic concurrency: testing/synctest.

Security

  • SQL injection: parameterized queries only, always. No string concatenation into SQL, ever.
  • Path traversal: for user-supplied paths, confine with os.Root (Go 1.24): root, err := os.OpenRoot(dir) then f, err := root.Open(name) — it refuses escapes via ../symlinks. Don't hand-roll filepath.Clean checks.
  • Templates: render HTML with html/template (contextual auto-escaping), never text/template, for anything reaching a browser.
  • Secrets/tokens: crypto/rand only. Use rand.Text() (Go 1.24) for URL-safe random strings and crypto/rand.Read for raw bytes. math/rand/math/rand/v2 is never acceptable for anything security-sensitive.
  • Command execution: exec.CommandContext(ctx, name, args...) with a fixed binary and separate args — never sh -c with interpolated user input.
  • Passwords: golang.org/x/crypto/bcrypt (or argon2id) — never SHA/MD5.
  • TLS: default config; if you must set it, MinVersion: tls.VersionTLS12. Never InsecureSkipVerify: true outside tests.
  • Set ReadHeaderTimeout (above), cap request bodies with http.MaxBytesReader, and validate/whitelist all inbound input at the boundary.
  • Scan every build: govulncheck ./... (install as a tool dep) in CI; it reports only vulns on reachable call paths.

Observability & profiling

  • Profiling: runtime/pprof for CPU/heap/mutex/block/goroutine profiles; analyze with go tool pprof. In tests/benchmarks capture via go test -cpuprofile cpu.out -memprofile mem.out -bench .. Set runtime.SetMutexProfileFraction/SetBlockProfileRate before profiling contention — they're off by default.
  • On servers, expose profiles behind a guarded admin listener: import _ "net/http/pprof" registers on the default mux, so never mount it on your public ServeMux — run it on a separate loopback/internal http.Server. It exposes execution details; treat it as privileged.
  • Execution tracing: runtime/trace (trace.Start(w)/trace.Stop, view with go tool trace) for scheduler latency, GC pauses, and goroutine blocking that pprof's sampling misses. Add trace.Region/trace.Log around hot spans; the flight recorder (trace.NewFlightRecorder, stable in 1.25) keeps a rolling in-memory window you snapshot on an anomaly.
  • Metrics: read the runtime via runtime/metrics (/gc/heap/allocs:bytes, /sched/goroutines:goroutines, …) — richer and cheaper than the legacy runtime.ReadMemStats. Publish app counters with expvar for a zero-dep JSON endpoint, or Prometheus (prometheus/client_golang) when you already run that stack. Don't hand-roll atomics you then have to expose.
  • Tracing/telemetry: OpenTelemetry (go.opentelemetry.io/otel) for distributed traces; propagate the span through the same context.Context you already thread. Correlate logs by putting the trace/request ID into slog attributes (slog.With("trace_id", id)), so logs, traces, and metrics share one key.

Do

  • Return early; keep the happy path at the leftmost indentation.
  • Make the zero value useful (var b bytes.Buffer works unconfigured); avoid mandatory init calls.
  • Accept context.Context first, return error last.
  • Preallocate with capacity when the size is known: make([]T, 0, n).
  • Use defer for cleanup, but hoist it out of hot loops (deferred calls run at function return, not loop-iteration end).
  • Keep the exported surface minimal; unexport anything callers don't need.
  • Run gofmt, go vet, golangci-lint run, and go test -race before declaring done.

Avoid

  • interface{}any. Empty-interface soup / type switches over uniform data → generics.
  • GOPATH, dep, vendor-by-default, tools.go → modules + tool directive.
  • Naked go func() with no lifecycleerrgroup or wg.Go + ctx.
  • x := x loop copies, for i := 0; i < len(s); i++ to iterate → range; the 1.22 loop-var fix and iterators made these obsolete.
  • fmt.Errorf("...: %v", err) (drops the chain) → %w. errors.New(fmt.Sprintf(...))fmt.Errorf.
  • err.Error() string matching, if err == someWrappedErrerrors.Is/As.
  • panic/log.Fatal in libraries → return errors; fail only in main.
  • http.ListenAndServe / http.DefaultClient with no timeouts → a configured http.Server/http.Client.
  • interface{ Do() } defined next to its only implementation → define it at the consumer.
  • Gorilla/mux or a framework for basic routingnet/http method patterns (1.22+).
  • math/rand for tokens, text/template for HTML, md5/sha1 for passwordscrypto/rand, html/template, bcrypt/argon2id.
  • time.Sleep to sequence goroutines in teststesting/synctest / channels.
  • ioutil.* (deprecated since 1.16) → io/os equivalents.

When you code

  • Ship the smallest diff that solves the task. Match the file's existing conventions before importing new ones.
  • Add no dependency without cause — check whether the stdlib already covers it. If you add one, justify it in a line and pin it in go.mod.
  • After every change, run gofmt -l . (must be empty), go build ./..., go vet ./..., golangci-lint run, and go test ./... -race. Report failures; don't paper over a lint finding with //nolint unless you explain why in the directive.
  • Don't invent APIs — if unsure a stdlib symbol exists in 1.26, say so rather than guessing.
  • Ask before: changing a public/exported API or the go.mod module path, adding a dependency or framework, altering DB schema/migrations, or introducing goroutines/concurrency into previously sequential code. Otherwise proceed and summarize what you changed and which checks you ran.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Go 1.26 · stdlib net/http · database/sql + pgx/v5 · golangci-lint v2.12.2.

Back to top ↑