Go

Writes, debugs, and reviews Go: goroutine leaks, data races, nil interfaces, slices and maps, go.mod, and net/http. Use when Go panics or misbehaves — "all goroutines are asleep - …

Iván

@ivangdavila

Install

$ openclaw skills install @ivangdavila/go

User preferences live in ~/Clawic/data/go/config.yaml (see Configuration); nothing else is stored on the user's machine. If you have data at an old location (~/go/ or ~/clawic/go/), move it to ~/Clawic/data/go/. Note that ~/go/ may also be the user's GOPATH — never write skill data there, and never delete it.

When To Use

  • Writing or reviewing Go — scan Core Rules and Output Gates before committing
  • Any panic, fatal error:, or race warning whose cause is not obvious in ten seconds
  • Hangs, goroutine growth, rising RSS, and CPU that appears without traffic
  • Concurrency design: channels vs mutexes, worker pools, cancellation, fan-in/fan-out
  • Toolchain and dependency work: modules, versions, build tags, cross-compilation, containers
  • API and package design decisions: interfaces, generics, error contracts, layout
  • Not for Kubernetes operators, gRPC service design, or web-framework internals — those live in their own skills

Quick Reference

SituationPlay
Goroutine count only goes up; process never releases memoryEvery go needs an exit path: closed channel, ctx.Done(), or a bounded loop → concurrency.md
WARNING: DATA RACE, or "it only fails under load"Read both stacks in the report; the fix is a mutex, a channel, or atomic — never a time.Sleepconcurrency.md
A call hangs forever, or shutdown never completesMissing deadline or a cancel() that was never deferred → context.md
errors.Is/errors.As stopped matching after a refactorSomeone wrapped with %v instead of %w, or dropped Unwrap()errors.md
A returned error is non-nil but holds nothingConcrete nil pointer in an interface: nil only when type AND value are nil → interfaces.md
Deciding between a type parameter, an interface, and anyInterface first; type parameters when the body is identical for every type → generics.md
Append mutated data someone else was holdingb := a[1:3] shares the array; cap(a[i:j]) == cap(a)-icollections.md
Byte/rune confusion, len() disagreeing with what you see, slow concatenationrange yields runes; strings.Builder for loops → strings.md
Struct copies, embedding, tags, map keys that panic, oversized structsComparability, promotion, and field alignment → structs.md
Timers, tickers, DST, parsing 2006-01-02, elapsed time that goes negativeMonotonic clock for durations, wall clock for instants → time.md
JSON decodes but fields are zero, big integers lose digits, omitempty misfiresExported fields + tags; numbers land as float64 in anyjson.md
Writing handlers or clients: timeouts, middleware, graceful shutdown, leaked bodiesNever http.DefaultClient in a service; drain and close every body → http.md
database is locked, connection pool exhaustion, NULLs, transactions that leakrows.Close() on every path; size the pool against the DB limit → database.md
Reading/writing files and streams; Close() errors; partial writesio.Reader/io.Writer composition; write temp then os.Renameio.md
Building a CLI: flags, exit codes, signals, piping, stdin detectionflag for simple, exit through main so defers run → cli.md
Nothing appears in the logs; deciding what to log and at which levellog/slog handler and level; attributes, not formatted strings → logging.md
Writing tests, table tests, fakes, fuzzing, benchmarks, golden filesSubtests + t.Parallel(); -race on the suite → testing.md
A panic stack, a hung binary, or a flake you cannot reproduceSIGQUIT stack dump, -race, GODEBUG, delve → debugging.md
"It is slow" — before optimizing anythingBenchmark, then pprof; fix the top line or nothing → performance.md
RSS grows, GC runs constantly, or a container gets OOM-killedGOGC, GOMEMLIMIT, escape analysis, retained references → memory.md
go.mod/go.sum errors, version selection, replace, private repos, monoreposMVS picks the minimum that satisfies everyone, not the latest → modules.md
Build tags, cross-compilation, go:embed, cgo, -ldflags, go generateCGO_ENABLED=0 GOOS=… GOARCH=… go buildbuild.md
Shipping the binary: container image, GOMAXPROCS in a cgroup, health, rolloutStatic binary + explicit resource ceilings → deployment.md
Untrusted input, secrets, tokens, TLS, templates, dependency CVEscrypto/rand, html/template, govulnchecksecurity.md
Naming, package boundaries, receivers, exported API shape, lintersPackage name is part of every identifier; no stutter → idioms.md
Choosing or upgrading a Go version; a feature that "should exist" but doesn'tTwo releases are supported at a time; upgrade with -race and vet first → versions.md
Anything elseCore Rules below, then reduce to a main.go that reproduces it in under 30 lines and add one thing back at a time

Each file above is one sub-job and is self-contained: read SKILL.md by default, open exactly one guide when the situation matches.

Core Rules

  1. No goroutine without a written exit path. A closed channel, a ctx.Done() case, or a loop with a bound — decided before the go keyword is typed. A handler that starts one un-cancellable goroutine per request at 1,000 rps leaks 3.6M goroutines an hour; at ~2 KB of initial stack each that is the whole heap (concurrency.md).
  2. Check the error at the call site; wrap with %w when you add context. fmt.Errorf("load config %s: %w", path, err). %v flattens the chain and silently breaks errors.Is/errors.As for every caller above you. Ignoring an error needs _ = plus a written reason (errors.md).
  3. An interface is nil only when its type word AND its value word are nil. var e *MyErr = nil; var err error = eerr != nil is true and every if err != nil above fires. Declare the return as error and return the literal nil, never a typed nil (interfaces.md).
  4. ctx context.Context is the first parameter of every call that can block, and every WithCancel/WithTimeout is followed by defer cancel(). Skipping cancel() leaks the timer and the child context until the parent finishes — go vet's lostcancel catches the common shape (context.md).
  5. defer fires at function return, not at end of block. for ... { rows, _ := db.Query(...); defer rows.Close() } holds every result set until the function returns and exhausts the pool. Move the body into its own function, or call Close() explicitly at the end of the iteration (io.md).
  6. Any variable touched by two goroutines needs a mutex, a channel, or sync/atomic. "It is just an int" is the most common data race in Go. go test -race proves nothing about code paths it did not execute, so run it on the suite, not on one test (debugging.md).
  7. Reslicing shares the array; appending may or may not copy. b := a[1:3]; append(b, x) overwrites a[3] whenever len(b) < cap(b), and cap(a[i:j]) == cap(a) - i. Force a copy with the three-index form a[1:3:3], which sets cap to len so the next append must reallocate (collections.md).
  8. Zero values are usable except for maps and channels. var s []int appends fine and marshals as null; var m map[string]int reads fine and panics on write; var ch chan int blocks forever on both send and receive. make maps and channels; leave slices, sync.Mutex, bytes.Buffer, and strings.Builder at zero.
  9. Loop-variable capture is gated on the go line in go.mod, not on the toolchain. With go >=1.22 the variable is per-iteration, so for _, v := range xs { go f(v) } is correct; below that line every goroutine sees the last element, even when built with a new compiler. Check the go directive before trusting the pattern (versions.md).

Version Floors

The floors that shape everyday Go. Individual guides carry more floors inline in this same go >=X.Y form, next to the instruction each one gates. Release policy, upgrade procedure, and removals: versions.md.

FeatureNeeds
errors.Is/As/Unwrap and %w wrappinggo >=1.13
go:embed, io/fs, os.ReadFile/WriteFile, go install pkg@version, modules on by defaultgo >=1.16
//go:build lines (replacing // +build), module graph pruninggo >=1.17
Generics, any, native fuzzing, workspaces (go work)go >=1.18
GOMEMLIMIT soft memory limit, GC CPU limitergo >=1.19
errors.Join, context.WithCancelCause, coverage for binaries (go build -cover)go >=1.20
slices, maps, cmp, log/slog; min/max/clear builtins; context.AfterFunc/WithoutCancel; toolchain directivego >=1.21
Per-iteration loop variables, for i := range 10, net/http method+wildcard routing patterns, math/rand/v2go >=1.22
Range-over-function iterators (iter.Seq), maps.Keys/Values as iterators, unreferenced timers collectable without Stopgo >=1.23
Generic type aliases, os.Root for directory-scoped file access, testing.B.Loop, go tool directivego >=1.24
Container-aware default GOMAXPROCS from the cgroup CPU limit, testing/synctestgo >=1.25

Panics And Fatal Errors

panic unwinds and runs defers, so recover() can catch it. fatal error: does not: the runtime kills the process immediately and no defer, recover, or handler sees it. Knowing which one you got halves the search.

MessageCauseFirst move
assignment to entry in nil mapMap declared but never madem := map[K]V{}; struct fields need it in the constructor
send on closed channelA second sender, or the receiver closedOnly the sole sender closes; stop senders with a done channel (concurrency.md)
close of closed channelTwo owners closingOne owner, or sync.Once around the close
fatal error: all goroutines are asleep - deadlock!Every goroutine is blocked — fires only in that total case, so partial deadlocks stay silent foreverDump all stacks with SIGQUIT and find the blocked pair (debugging.md)
fatal error: concurrent map writesUnsynchronized map access; not recoverable, and it is always a real bugMutex around the map, or sync.Map for its two documented shapes (concurrency.md)
invalid memory address or nil pointer dereferenceNil pointer, or a method promoted from an embedded nil pointerSuspect the receiver, not only the arguments (structs.md)
index out of range [5] with length 3Off-by-one, or a length read before a concurrent writeThe message prints both numbers — compare them before reading code
interface conversion: interface {} is string, not intType assertion without comma-okv, ok := x.(T) (interfaces.md)
comparing uncomparable typeA struct containing a slice, map, or func used as a map key through an interfaceKey on a derived comparable value (structs.md)
sync: negative WaitGroup counterDone() called more often than Add(), or Add() moved inside the goroutineAdd(1) before the go statement, always
WARNING: DATA RACETwo unsynchronized accesses, at least one a writeRead both stacks; the older one names the unprotected owner (debugging.md)
missing go.sum entry / ambiguous importDependency graph edited without tidying, or two modules provide the same pathgo mod tidy, then go mod why on the loser (modules.md)
declared and not used / imported and not usedCompile error by design, not a lintDelete it; _ = x belongs only in generated code

Output Gates

Before delivering Go code, check:

  • Every go statement has a stated exit path, and every WithCancel/WithTimeout has defer cancel()
  • Every error is checked; context added with %w; every discarded error has _ = and a written reason
  • ctx is the first parameter of every exported call that blocks, and every network, database, lock, and subprocess call has a deadline
  • No exported function returns a concrete error type as error; no typed nil escapes as an interface
  • Every acquired resource closes on every path, including the error paths, and Close() errors on writers are checked
  • go vet is clean and go test -race ./... ran at least once against the changed packages
  • Syntax and stdlib stay within min_go, and the go directive in go.mod actually allows what you emitted
  • Nothing untrusted reaches text/template for HTML, a concatenated SQL string, os/exec with a shell, or a path join without containment; no math/rand for anything secret

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/go/config.yaml. Never interview the user — record a preference the moment it is stated.

VariableTypeDefaultEffect
min_go1.21 | 1.22 | 1.23 | 1.24 | 1.251.22Gates which Version Floors features may be emitted unguarded, and which fallback appears instead (versions.md)
package_layoutflat | cmd-internalcmd-internalWhere new packages and files go, and whether internal/ is used (idioms.md)
test_stylestdlib | testifystdlibAssertion, fake, and mock style in emitted tests (testing.md)
http_stacknet/http | chi | gin | echonet/httpRouter, middleware, and handler idiom in emitted server code (http.md)
loggerslog | zap | zerolog | logslogLogging calls, handler setup, and how context is carried into log lines (logging.md)
error_wrappingalways | boundaries | neveralwaysHow much %w context is added: every hop, only at package boundaries, or opaque errors only (errors.md)
cgoenabled | disableddisabledWhether builds assume CGO_ENABLED=0 static binaries; gates cross-compile, Alpine, and DNS-resolver advice (build.md)
target_platformlinux/amd64 | linux/arm64 | darwin/arm64 | windows/amd64 | crosslinux/amd64Assumed GOOS/GOARCH in build, deploy, and file-path guidance; cross flags every platform-specific assumption
change_workflowtest-first | fix-firsttest-firstOrder of the work: test-first writes the failing test before the fix, fix-first patches first and backfills the test against the reverted change (testing.md)
upgrade_cadencecontinuous | monthly | quarterly | security-onlymonthlyHow eagerly toolchain and dependency upgrades are proposed, and how big a batch: continuous takes each release as it lands, quarterly batches them, security-only moves for govulncheck findings and patch releases and nothing else. Also decides whether "one release behind latest" is reported as fine or as debt (versions.md, modules.md)
default_timeoutnumber (seconds, 1-300)10The budget emitted when the caller states none. http.Client.Timeout = this value; the server set scales off it — ReadHeaderTimeout ×0.5, ReadTimeout ×1.5, WriteTimeout ×3, IdleTimeout ×6 (10s → 5/15/30/60s); every per-call context.WithTimeout with no stated deadline uses it (http.md)

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

  • Tooling: linter set (go vet only, golangci-lint standard or strict), mock generator, migration tool, task runner, go work vs one module per repo — affects every emitted config block and command (idioms.md)
  • Conventions: receiver names and pointer-vs-value policy, error message phrasing, table-test naming, comment and doc style, struct-tag conventions — affects generated code and review comments
  • Platform: deployment target (container, serverless, CLI distribution, embedded), CPU architecture, private module proxy, CI provider — affects deployment.md and modules.md guidance
  • Safety posture: how aggressively to flag missing timeouts, unchecked errors, panic in library code, unbounded goroutines, and unpinned dependencies — affects security.md and review depth
  • Dependencies: stdlib-only constraints, banned or mandated libraries (pkg/errors vs stdlib, sqlx vs database/sql, ORM policy), tolerance for new transitive dependencies — affects every recommendation
  • Output format: whole file vs minimal diff, how much explanation, whether tests and benchmarks accompany every change — affects the shape of the answer, never the correctness rules
  • Work order: which gates run before a change is proposed rather than after — go vet and -race green first, a benchmark before any optimization, a plan approved before restructuring packages — affects the sequence of every task, never the correctness rules
  • Cadence: beyond upgrade_cadence — the day and batch size for go get -u runs, whether a new Go release is adopted in its first weeks or after .1, how often govulncheck and go mod tidy run in CI, and the review interval for pinned replace directives — affects versions.md and modules.md, never the correctness rules
  • Thresholds and sizing: the numbers a team standardises beyond default_timeoutSetMaxOpenConns/SetMaxIdleConns against the database's own connection cap divided by instance count, GOMEMLIMIT headroom (default ~90% of the container limit), benchmark -count (default 10) and the benchstat delta that counts as a win, http.MaxBytesReader body cap (default 1 MiB), worker-pool width — affects which value gets emitted, while Safety posture governs whether a missing one is flagged

Traps

TrapWhy it failsDo instead
http.Get or http.DefaultClient in a serviceIts timeout is zero: one unresponsive server pins a goroutine and a connection until the process diesOwn client with an explicit Timeout and a tuned Transport (http.md)
defer resp.Body.Close() without drainingAn unread body cannot be returned to the pool, so every call opens a fresh connectionio.Copy(io.Discard, resp.Body) before Close() (http.md)
time.After inside a select loopAllocates a timer per iteration; on go <1.23 each one also stayed alive until it firedOne time.Timer with Reset, or context.WithTimeout (time.md)
Passing sync.WaitGroup, sync.Mutex, or a struct containing one by valueThe copy has its own counter or lock, so Wait returns early and mutual exclusion is gonePass the pointer; go vet's copylocks catches it
_ = json.Unmarshal(b, &v) on lowercase struct fieldsencoding/json only sees exported fields — no error, all zerosExport the fields and tag them (json.md)
panic on an expected failure in a libraryTurns the caller's recoverable case into a process kill they cannot handleReturn an error; reserve panic for programmer bugs (errors.md)
math/rand for tokens, IDs, or passwordsPredictable from a few outputs regardless of seedingcrypto/rand (security.md)
os.Exit or log.Fatal below mainDeferred cleanup, flushes, and traces never runReturn the error up to main and exit there (cli.md)
init() that reads files, dials, or reads envRuns on import — in every test binary, in every tool that links the package, in an order you do not controlExplicit constructor called from main
t.Setenv in a test that also calls t.Parallel()Panics by design: the environment is process-wideInject config, or keep that test serial (testing.md)
interface{}/any in a signature "to stay flexible"Moves a compile error to a runtime assertion in someone else's codeA small interface, or a type parameter (generics.md)
Storing context.Context in a struct fieldFreezes one request's deadline into a long-lived object; cancellation stops matching the callerPass it per call (context.md)
time.Now().Sub(start) after a value round-tripMarshaling or Round strips the monotonic reading, so NTP steps can make elapsed time negativetime.Since(start) on an untouched time.Time (time.md)
Goroutine that only sends on an unbuffered channel nobody reads after an early returnBlocks forever and holds every variable it capturedBuffer of one, or select with ctx.Done() (concurrency.md)

Where Experts Disagree

  • Channels vs mutexes. "Share memory by communicating" is the slogan; the Go team's own guidance is that a mutex around shared state is often simpler and faster. Boundary: channels when ownership of a value moves between goroutines or you need cancellation and fan-out; a sync.Mutex when several goroutines read and write one struct in place. A channel used purely as a lock is a slower lock (concurrency.md).
  • Project layout. The golang-standards/project-layout tree (pkg/, api/, build/) is community folklore, not a Go team standard, and the standard library itself is flat. Boundary: a single binary starts flat and grows internal/ when a package must not be imported outside; multi-binary repos earn cmd/. Ceremony before the second binary exists is cost without benefit (idioms.md).
  • How opaque errors should be. One school wraps at every hop so callers can errors.Is anything; the other treats error identity as API surface and exports only a few sentinels, hiding the rest behind behavioral checks. Boundary: within one module, wrap freely; across a published API, every exported sentinel and error type is a compatibility promise you cannot retract (errors.md).
  • Generics vs interfaces. Type parameters remove any from container and algorithm code, but Go compiles them with GC-shape stenciling: all pointer-shaped instantiations share one body plus a dictionary, so generic code over pointers can call indirectly where an interface would too. Boundary: identical logic across types that share no methods → type parameters; behavior that differs per type → interface (generics.md).

Related Skills

More Clawic skills, get them at https://clawic.com/skills/go (install if the user confirms):

  • concurrency — language-agnostic memory model: happens-before, atomics, lock-free tradeoffs
  • debugging — hypothesis-driven fault isolation; the Go-specific version of that job is this skill's debugging.md
  • profiling — choosing the right clock and reading flame graphs, whatever the language
  • docker — building and running the container the Go binary ships in
  • api-design — the HTTP contract itself, above the handler code

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/go.

Related skills

Python

@ivangdavila

Writes, debugs, and reviews Python code — runtime traps, packaging, typing, async, tests, performance. Use when Python raises or misbehaves: ModuleNotFoundError, circular imports, AttributeError on None, UnboundLocalError, UnicodeDecodeError, mutable default arguments, `is` vs `==`, float rounding, naive vs aware datetimes, or a wrong answer with no exception; when pip, uv, poetry, venv, pyproject or a lockfile fight over dependencies, or a package installs but will not import; when threads, asyncio, multiprocessing or the GIL hang, deadlock, or leak memory; when pytest passes but should not, mocks patch the wrong module, or async tests never run; when mypy or pyright errors need clearing; when a script is slow, eats RAM, or gets OOM-killed; when subprocess calls hang or logs never appear; or when upgrading Python breaks the build. Not for library-specific problems — pandas, numpy, django, fastapi and flask have their own skills.

55.0k

TypeScript

@ivangdavila

Writes, reviews, and debugs type-safe TypeScript: type errors, narrowing, generics, tsconfig strictness, and declaration files. Use when a build hits type errors ("not assignable", "possibly undefined", "excessively deep", "cannot find module"), when eliminating any or unchecked as casts, designing generics or discriminated unions, writing .d.ts files or typing untyped npm packages, configuring tsconfig or module resolution, fixing ESM/CJS import crashes ("ERR_REQUIRE_ESM", "x is not a function"), validating API or JSON data at runtime, migrating JavaScript to TypeScript or upgrading the TypeScript version, or when tsc or the editor gets slow or runs out of memory.

65.8k

linux

@ivangdavila

Debugs and hardens Linux hosts: permissions, disk full, OOM kills, stuck processes, systemd units, cron, networking, SSH, and boot failures. Use when a service starts by hand but fails at boot, a process ignores kill -9, df and du disagree, a box runs out of memory or inodes, sudo or ACLs deny access, SELinux blocks a write, a job works in the shell but not in cron, sshd rejects a key, an upgrade leaves packages half-configured, load is high while the CPU sits idle, or a host needs firewall rules, users, LVM, journald, kernel tuning, or a security baseline. Covers Debian/Ubuntu, RHEL/Fedora, Arch, and Alpine. Not for shell-script syntax (bash) or container build and runtime internals (docker).

84.9k

react

@ivangdavila

Builds, debugs, and reviews React apps: components, hooks, state, Server Components, forms, performance, testing. Use when writing or refactoring components, choosing state management (useState, Context, Zustand, TanStack Query, Redux), when a component rerenders too often, loops infinitely ("too many re-renders"), shows stale or not-updating state, fails with a hydration mismatch or hook-order error, when an effect fires twice or fetches race, when typing lags or long lists scroll slowly, when adopting React 19, Server Actions, or the React Compiler, when testing components with Testing Library, typing props in TypeScript, or reviewing AI-generated React code. Not for React Native (mobile) or Next.js routing and deployment — use react-native or nextjs.

37.8k

Designer

@ivangdavila

Operates as the designer on a job: decides what to make, makes it, defends it in review, and hands over files that can actually be built. Use when acting as the designer for a product, a brand, or a client; when a logo, interface, landing page, app icon, deck, or printed piece has to be created and justified; when a screen needs its empty, loading, error, hover, focus, and disabled states pinned down; when contrast, target size, focus order, or reduced motion fails a check; when a palette, type scale, spacing scale, or token set has to be defined and named; when engineers shipped something that does not match the mockup; when running a critique, a usability test, or a stakeholder presentation; when scoping a brief, revision rounds, or a rebrand. Covers dark mode, iOS and Android conventions, email and print production. Not for one-off visual judgment on a single artifact (`design`), chart design (`data-visualization-design`), or front-end implementation (`frontend`).

45.4k

docker

@ivangdavila

Builds, debugs, and hardens Docker containers, images, and Compose stacks. Use when writing Dockerfiles or compose files, when a container crashes, restart-loops, gets OOM-killed, cannot reach the network, or fills the disk, when builds are slow or fail only in CI, when choosing base images or pinning, or when preparing containers and hosts for production.

2515k