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.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou 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.26ingo.modand pin the toolchain withtoolchain go1.26.4. Use Go modules only — noGOPATHlayout, nodep/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 floatingv2drifts lint results between runs. Format withgolangci-lint fmt(gofmt + goimports + gofumpt live under the v2formatters:block, notlinters-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 inlinedirectives to migrate off deprecated constants/wrappers. - HTTP: stdlib
net/httpwith the 1.22+ method+patternServeMux. Addchiv5 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(usepgxnative pool for Postgres-heavy code; thestdlibadapter when you needdatabase/sql). Migrations viapressly/gooseorgolang-migrate. No heavyweight ORM; prefersqlcto generate typed queries from SQL over GORM. - Logging: stdlib
log/slog(structured). Nologrus/zapin new code unless perf-profiled to need it. - Tooling deps: track with the
tooldirective ingo.mod(go get -tool <pkg>, run viago tool <name>). Delete any legacytools.gobuild-tag file. - Config/CLI: stdlib
flagfor simple binaries;spf13/cobrafor multi-command CLIs. Env viaos.Getenvorcaarlos0/env, not a global singleton.
Project conventions
- Layout:
cmd/<binary>/main.goper executable; importable code underinternal/(compiler-enforced privacy) or the module root; nopkg/cargo-cult directory. Keepmainthin — parse flags, wire deps, call into packages. - Package names: short, lowercase, no underscores, no plurals, singular noun (
user, notusers/userutils). Ban grab-bag packagesutil,common,helpers,base,misc,shared. - No stutter: the package qualifies the name. Export
user.Store, notuser.UserStore;http.Server, nothttp.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 fmtenforce it. The local module path is the import prefix. - Exported identifiers get doc comments starting with the identifier name (
// Server handles ...).go vetandrevivecheck this. - Constructors return the concrete type:
func New(...) *Server. Returnerroras 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 errornoise. Add%wonly where it adds locating information. - Inspect with
errors.Is(err, ErrNotFound)(sentinel) anderrors.As(err, &target)(typed). Never compare with==across wrapped errors orstrings.Contains(err.Error(), ...). - Sentinels:
var ErrNotFound = errors.New("not found")at package scope. Custom types implementError() stringand, when wrapping,Unwrap() error. - Aggregate independent failures with
errors.Join(err1, err2)(both stay discoverable viaIs/As). - Never discard
err—errcheckflags_ = f(). Handle, return, or log; if truly ignorable, comment why. Always checkdeferclosers on writers:defer func() { err = errors.Join(err, f.Close()) }()for files you wrote to. - No
panicin library code. Panic only for genuinely unreachable invariants (panic("unreachable")); recover only at goroutine/request boundaries and convert to an error. Return errors, don'tlog.Fataloutsidemain. - 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)declarestype 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 aliasinterface{}. Reach for generics ([T any],[T cmp.Ordered]) instead ofany+ 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.Contextis 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 passnil— usecontext.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/errgroupfor fan-out with first-error + cancellation. Useg.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 manualAdd(1)/defer Done(). - Channels to hand off ownership/stream;
sync.Mutexto 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.OnceFuncfor lazy init instead of a hand-rolledonce.Do+ package var. - Timeouts/cancellation:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel(). Alwaysdefer cancel()even on the success path. - Graceful shutdown in
main:ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM); defer stop(), thensrv.Shutdown(ctx). - Run tests and CI with
-race. Guard time-dependent concurrency tests withtesting/synctest(stable in 1.25) rather thantime.Sleep.
Prefer stdlib
- Routing (1.22+): register method+path patterns on
http.ServeMux; read params withr.PathValue:mux := http.NewServeMux() mux.HandleFunc("GET /items/{id}", h.get) mux.HandleFunc("POST /items", h.create) id := r.PathValue("id") - Never call
http.ListenAndServebare. Build anhttp.Server{Handler, ReadHeaderTimeout: 5*time.Second, ReadTimeout, WriteTimeout, IdleTimeout}— an unsetReadHeaderTimeoutis a Slowloris DoS. SetBaseContextto your shutdown context. - HTTP clients: never use
http.DefaultClientfor external calls with no timeout. Construct&http.Client{Timeout: ...}and drive per-request deadlines withhttp.NewRequestWithContext. - DB: always the context methods —
QueryContext,ExecContext,QueryRowContext,BeginTx. Parameterize ($1/?); neverfmt.Sprintfvalues into SQL. Configure the pool:SetMaxOpenConns,SetMaxIdleConns,SetConnMaxLifetime.defer rows.Close()and checkrows.Err()after the loop. - JSON:
encoding/json. Usejson:",omitzero"(Go 1.24) for clean optional-field omission — it fires on the true zero value /IsZero(), fixingomitempty's inability to omit zero structs andtime.Time. Decode withjson.NewDecoder(r.Body)and calldec.DisallowUnknownFields()for strict APIs.encoding/json/v2exists but is stillGOEXPERIMENT=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(returniter.Seq),Clone,Equal.cmp:Compare,Or,Less.- Iterators (Go 1.23): range over
iter.Seq[T]/iter.Seq2[K,V]. Consumemaps.Keys(m)directly withfor k := range maps.Keys(m); useslices.Collect/maps.Collectto materialize. Preferstrings.Lines/strings.SplitSeq/strings.FieldsSeq(1.24) over allocatingSplit. for i := range n {}to loop a fixed count (Go 1.22). Loop variables are per-iteration since 1.22 — delete everyx := xshadow copy.log/slogfor logs:slog.InfoContext(ctx, "msg", "key", val); set a JSON handler inmain.time: prefertime.Since/time.Until. Compare instants with.Equal, not==.
Testing
go test ./... -race -shuffle=on. Standard librarytestingfirst; addgoogle/go-cmpfor deep struct diffs (cmp.Diff(want, got)) andtestify/requireonly where its assertions materially cut noise — don't pull a BDD framework.- Table-driven tests, subtests via
t.Run(tc.name, ...), andt.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 andt.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 witherrors.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 oldfor 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)thenf, err := root.Open(name)— it refuses escapes via../symlinks. Don't hand-rollfilepath.Cleanchecks. - Templates: render HTML with
html/template(contextual auto-escaping), nevertext/template, for anything reaching a browser. - Secrets/tokens:
crypto/randonly. Userand.Text()(Go 1.24) for URL-safe random strings andcrypto/rand.Readfor raw bytes.math/rand/math/rand/v2is never acceptable for anything security-sensitive. - Command execution:
exec.CommandContext(ctx, name, args...)with a fixed binary and separate args — neversh -cwith 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. NeverInsecureSkipVerify: trueoutside tests. - Set
ReadHeaderTimeout(above), cap request bodies withhttp.MaxBytesReader, and validate/whitelist all inbound input at the boundary. - Scan every build:
govulncheck ./...(install as atooldep) in CI; it reports only vulns on reachable call paths.
Observability & profiling
- Profiling:
runtime/pproffor CPU/heap/mutex/block/goroutine profiles; analyze withgo tool pprof. In tests/benchmarks capture viago test -cpuprofile cpu.out -memprofile mem.out -bench .. Setruntime.SetMutexProfileFraction/SetBlockProfileRatebefore 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 publicServeMux— run it on a separate loopback/internalhttp.Server. It exposes execution details; treat it as privileged. - Execution tracing:
runtime/trace(trace.Start(w)/trace.Stop, view withgo tool trace) for scheduler latency, GC pauses, and goroutine blocking that pprof's sampling misses. Addtrace.Region/trace.Logaround 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 legacyruntime.ReadMemStats. Publish app counters withexpvarfor 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 samecontext.Contextyou already thread. Correlate logs by putting the trace/request ID intoslogattributes (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.Bufferworks unconfigured); avoid mandatory init calls. - Accept
context.Contextfirst, returnerrorlast. - Preallocate with capacity when the size is known:
make([]T, 0, n). - Use
deferfor 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, andgo test -racebefore declaring done.
Avoid
interface{}→any. Empty-interface soup / type switches over uniform data → generics.GOPATH,dep,vendor-by-default,tools.go→ modules +tooldirective.- Naked
go func()with no lifecycle →errgrouporwg.Go+ctx. x := xloop 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 == someWrappedErr→errors.Is/As.panic/log.Fatalin libraries → return errors; fail only inmain.http.ListenAndServe/http.DefaultClientwith no timeouts → a configuredhttp.Server/http.Client.interface{ Do() }defined next to its only implementation → define it at the consumer.- Gorilla/mux or a framework for basic routing →
net/httpmethod patterns (1.22+). math/randfor tokens,text/templatefor HTML,md5/sha1for passwords →crypto/rand,html/template,bcrypt/argon2id.time.Sleepto sequence goroutines in tests →testing/synctest/ channels.ioutil.*(deprecated since 1.16) →io/osequivalents.
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, andgo test ./... -race. Report failures; don't paper over a lint finding with//nolintunless 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.modmodule 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.