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/goUser 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
| Situation | Play |
|---|---|
| Goroutine count only goes up; process never releases memory | Every 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.Sleep → concurrency.md |
| A call hangs forever, or shutdown never completes | Missing deadline or a cancel() that was never deferred → context.md |
errors.Is/errors.As stopped matching after a refactor | Someone wrapped with %v instead of %w, or dropped Unwrap() → errors.md |
A returned error is non-nil but holds nothing | Concrete nil pointer in an interface: nil only when type AND value are nil → interfaces.md |
Deciding between a type parameter, an interface, and any | Interface first; type parameters when the body is identical for every type → generics.md |
| Append mutated data someone else was holding | b := a[1:3] shares the array; cap(a[i:j]) == cap(a)-i → collections.md |
Byte/rune confusion, len() disagreeing with what you see, slow concatenation | range yields runes; strings.Builder for loops → strings.md |
| Struct copies, embedding, tags, map keys that panic, oversized structs | Comparability, promotion, and field alignment → structs.md |
Timers, tickers, DST, parsing 2006-01-02, elapsed time that goes negative | Monotonic clock for durations, wall clock for instants → time.md |
JSON decodes but fields are zero, big integers lose digits, omitempty misfires | Exported fields + tags; numbers land as float64 in any → json.md |
| Writing handlers or clients: timeouts, middleware, graceful shutdown, leaked bodies | Never http.DefaultClient in a service; drain and close every body → http.md |
database is locked, connection pool exhaustion, NULLs, transactions that leak | rows.Close() on every path; size the pool against the DB limit → database.md |
Reading/writing files and streams; Close() errors; partial writes | io.Reader/io.Writer composition; write temp then os.Rename → io.md |
| Building a CLI: flags, exit codes, signals, piping, stdin detection | flag for simple, exit through main so defers run → cli.md |
| Nothing appears in the logs; deciding what to log and at which level | log/slog handler and level; attributes, not formatted strings → logging.md |
| Writing tests, table tests, fakes, fuzzing, benchmarks, golden files | Subtests + t.Parallel(); -race on the suite → testing.md |
| A panic stack, a hung binary, or a flake you cannot reproduce | SIGQUIT stack dump, -race, GODEBUG, delve → debugging.md |
| "It is slow" — before optimizing anything | Benchmark, then pprof; fix the top line or nothing → performance.md |
| RSS grows, GC runs constantly, or a container gets OOM-killed | GOGC, GOMEMLIMIT, escape analysis, retained references → memory.md |
go.mod/go.sum errors, version selection, replace, private repos, monorepos | MVS picks the minimum that satisfies everyone, not the latest → modules.md |
Build tags, cross-compilation, go:embed, cgo, -ldflags, go generate | CGO_ENABLED=0 GOOS=… GOARCH=… go build → build.md |
| Shipping the binary: container image, GOMAXPROCS in a cgroup, health, rollout | Static binary + explicit resource ceilings → deployment.md |
| Untrusted input, secrets, tokens, TLS, templates, dependency CVEs | crypto/rand, html/template, govulncheck → security.md |
| Naming, package boundaries, receivers, exported API shape, linters | Package name is part of every identifier; no stutter → idioms.md |
| Choosing or upgrading a Go version; a feature that "should exist" but doesn't | Two releases are supported at a time; upgrade with -race and vet first → versions.md |
| Anything else | Core 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
- No goroutine without a written exit path. A closed channel, a
ctx.Done()case, or a loop with a bound — decided before thegokeyword 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). - Check the error at the call site; wrap with
%wwhen you add context.fmt.Errorf("load config %s: %w", path, err).%vflattens the chain and silently breakserrors.Is/errors.Asfor every caller above you. Ignoring an error needs_ =plus a written reason (errors.md). - An interface is nil only when its type word AND its value word are nil.
var e *MyErr = nil; var err error = e→err != nilis true and everyif err != nilabove fires. Declare the return aserrorand return the literalnil, never a typed nil (interfaces.md). ctx context.Contextis the first parameter of every call that can block, and everyWithCancel/WithTimeoutis followed bydefer cancel(). Skippingcancel()leaks the timer and the child context until the parent finishes —go vet'slostcancelcatches the common shape (context.md).deferfires 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 callClose()explicitly at the end of the iteration (io.md).- 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 -raceproves nothing about code paths it did not execute, so run it on the suite, not on one test (debugging.md). - Reslicing shares the array; appending may or may not copy.
b := a[1:3];append(b, x)overwritesa[3]wheneverlen(b) < cap(b), andcap(a[i:j]) == cap(a) - i. Force a copy with the three-index forma[1:3:3], which setscaptolenso the next append must reallocate (collections.md). - Zero values are usable except for maps and channels.
var s []intappends fine and marshals asnull;var m map[string]intreads fine and panics on write;var ch chan intblocks forever on both send and receive.makemaps and channels; leave slices,sync.Mutex,bytes.Buffer, andstrings.Builderat zero. - Loop-variable capture is gated on the
goline in go.mod, not on the toolchain. Withgo >=1.22the variable is per-iteration, sofor _, 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 thegodirective 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.
| Feature | Needs |
|---|---|
errors.Is/As/Unwrap and %w wrapping | go >=1.13 |
go:embed, io/fs, os.ReadFile/WriteFile, go install pkg@version, modules on by default | go >=1.16 |
//go:build lines (replacing // +build), module graph pruning | go >=1.17 |
Generics, any, native fuzzing, workspaces (go work) | go >=1.18 |
GOMEMLIMIT soft memory limit, GC CPU limiter | go >=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 directive | go >=1.21 |
Per-iteration loop variables, for i := range 10, net/http method+wildcard routing patterns, math/rand/v2 | go >=1.22 |
Range-over-function iterators (iter.Seq), maps.Keys/Values as iterators, unreferenced timers collectable without Stop | go >=1.23 |
Generic type aliases, os.Root for directory-scoped file access, testing.B.Loop, go tool directive | go >=1.24 |
Container-aware default GOMAXPROCS from the cgroup CPU limit, testing/synctest | go >=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.
| Message | Cause | First move |
|---|---|---|
assignment to entry in nil map | Map declared but never made | m := map[K]V{}; struct fields need it in the constructor |
send on closed channel | A second sender, or the receiver closed | Only the sole sender closes; stop senders with a done channel (concurrency.md) |
close of closed channel | Two owners closing | One 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 forever | Dump all stacks with SIGQUIT and find the blocked pair (debugging.md) |
fatal error: concurrent map writes | Unsynchronized map access; not recoverable, and it is always a real bug | Mutex around the map, or sync.Map for its two documented shapes (concurrency.md) |
invalid memory address or nil pointer dereference | Nil pointer, or a method promoted from an embedded nil pointer | Suspect the receiver, not only the arguments (structs.md) |
index out of range [5] with length 3 | Off-by-one, or a length read before a concurrent write | The message prints both numbers — compare them before reading code |
interface conversion: interface {} is string, not int | Type assertion without comma-ok | v, ok := x.(T) (interfaces.md) |
comparing uncomparable type | A struct containing a slice, map, or func used as a map key through an interface | Key on a derived comparable value (structs.md) |
sync: negative WaitGroup counter | Done() called more often than Add(), or Add() moved inside the goroutine | Add(1) before the go statement, always |
WARNING: DATA RACE | Two unsynchronized accesses, at least one a write | Read both stacks; the older one names the unprotected owner (debugging.md) |
missing go.sum entry / ambiguous import | Dependency graph edited without tidying, or two modules provide the same path | go mod tidy, then go mod why on the loser (modules.md) |
declared and not used / imported and not used | Compile error by design, not a lint | Delete it; _ = x belongs only in generated code |
Output Gates
Before delivering Go code, check:
- Every
gostatement has a stated exit path, and everyWithCancel/WithTimeouthasdefer cancel() - Every error is checked; context added with
%w; every discarded error has_ =and a written reason ctxis 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 vetis clean andgo test -race ./...ran at least once against the changed packages- Syntax and stdlib stay within
min_go, and thegodirective in go.mod actually allows what you emitted - Nothing untrusted reaches
text/templatefor HTML, a concatenated SQL string,os/execwith a shell, or a path join without containment; nomath/randfor 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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| min_go | 1.21 | 1.22 | 1.23 | 1.24 | 1.25 | 1.22 | Gates which Version Floors features may be emitted unguarded, and which fallback appears instead (versions.md) |
| package_layout | flat | cmd-internal | cmd-internal | Where new packages and files go, and whether internal/ is used (idioms.md) |
| test_style | stdlib | testify | stdlib | Assertion, fake, and mock style in emitted tests (testing.md) |
| http_stack | net/http | chi | gin | echo | net/http | Router, middleware, and handler idiom in emitted server code (http.md) |
| logger | slog | zap | zerolog | log | slog | Logging calls, handler setup, and how context is carried into log lines (logging.md) |
| error_wrapping | always | boundaries | never | always | How much %w context is added: every hop, only at package boundaries, or opaque errors only (errors.md) |
| cgo | enabled | disabled | disabled | Whether builds assume CGO_ENABLED=0 static binaries; gates cross-compile, Alpine, and DNS-resolver advice (build.md) |
| target_platform | linux/amd64 | linux/arm64 | darwin/arm64 | windows/amd64 | cross | linux/amd64 | Assumed GOOS/GOARCH in build, deploy, and file-path guidance; cross flags every platform-specific assumption |
| change_workflow | test-first | fix-first | test-first | Order 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_cadence | continuous | monthly | quarterly | security-only | monthly | How 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_timeout | number (seconds, 1-300) | 10 | The 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 vetonly, golangci-lint standard or strict), mock generator, migration tool, task runner,go workvs 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.mdandmodules.mdguidance - Safety posture: how aggressively to flag missing timeouts, unchecked errors,
panicin library code, unbounded goroutines, and unpinned dependencies — affectssecurity.mdand review depth - Dependencies: stdlib-only constraints, banned or mandated libraries (
pkg/errorsvs stdlib,sqlxvsdatabase/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 vetand-racegreen 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 forgo get -uruns, whether a new Go release is adopted in its first weeks or after.1, how oftengovulncheckandgo mod tidyrun in CI, and the review interval for pinnedreplacedirectives — affectsversions.mdandmodules.md, never the correctness rules - Thresholds and sizing: the numbers a team standardises beyond
default_timeout—SetMaxOpenConns/SetMaxIdleConnsagainst the database's own connection cap divided by instance count,GOMEMLIMITheadroom (default ~90% of the container limit), benchmark-count(default 10) and thebenchstatdelta that counts as a win,http.MaxBytesReaderbody cap (default 1 MiB), worker-pool width — affects which value gets emitted, while Safety posture governs whether a missing one is flagged
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
http.Get or http.DefaultClient in a service | Its timeout is zero: one unresponsive server pins a goroutine and a connection until the process dies | Own client with an explicit Timeout and a tuned Transport (http.md) |
defer resp.Body.Close() without draining | An unread body cannot be returned to the pool, so every call opens a fresh connection | io.Copy(io.Discard, resp.Body) before Close() (http.md) |
time.After inside a select loop | Allocates a timer per iteration; on go <1.23 each one also stayed alive until it fired | One time.Timer with Reset, or context.WithTimeout (time.md) |
Passing sync.WaitGroup, sync.Mutex, or a struct containing one by value | The copy has its own counter or lock, so Wait returns early and mutual exclusion is gone | Pass the pointer; go vet's copylocks catches it |
_ = json.Unmarshal(b, &v) on lowercase struct fields | encoding/json only sees exported fields — no error, all zeros | Export the fields and tag them (json.md) |
panic on an expected failure in a library | Turns the caller's recoverable case into a process kill they cannot handle | Return an error; reserve panic for programmer bugs (errors.md) |
math/rand for tokens, IDs, or passwords | Predictable from a few outputs regardless of seeding | crypto/rand (security.md) |
os.Exit or log.Fatal below main | Deferred cleanup, flushes, and traces never run | Return the error up to main and exit there (cli.md) |
init() that reads files, dials, or reads env | Runs on import — in every test binary, in every tool that links the package, in an order you do not control | Explicit constructor called from main |
t.Setenv in a test that also calls t.Parallel() | Panics by design: the environment is process-wide | Inject 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 code | A small interface, or a type parameter (generics.md) |
Storing context.Context in a struct field | Freezes one request's deadline into a long-lived object; cancellation stops matching the caller | Pass it per call (context.md) |
time.Now().Sub(start) after a value round-trip | Marshaling or Round strips the monotonic reading, so NTP steps can make elapsed time negative | time.Since(start) on an untouched time.Time (time.md) |
| Goroutine that only sends on an unbuffered channel nobody reads after an early return | Blocks forever and holds every variable it captured | Buffer 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.Mutexwhen 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-layouttree (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 growsinternal/when a package must not be imported outside; multi-binary repos earncmd/. 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.Isanything; 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
anyfrom 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 tradeoffsdebugging— hypothesis-driven fault isolation; the Go-specific version of that job is this skill'sdebugging.mdprofiling— choosing the right clock and reading flame graphs, whatever the languagedocker— building and running the container the Go binary ships inapi-design— the HTTP contract itself, above the handler code
Feedback
- If useful, star it: https://clawic.com/skills/go
- Latest version: https://clawic.com/skills/go
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/go.
Related skills
Python
@ivangdavilaWrites, 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.
TypeScript
@ivangdavilaWrites, 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.
linux
@ivangdavilaDebugs 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).
react
@ivangdavilaBuilds, 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.
Designer
@ivangdavilaOperates 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`).
docker
@ivangdavilaBuilds, 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.