Install
openclaw skills install @ivangdavila/goWrites, 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 - deadlock!", "concurrent map writes", "send on closed channel", "assignment to entry in nil map", or a WARNING: DATA RACE from go test -race; when a goroutine, timer, or connection leaks and memory climbs; when a service hangs for want of a timeout; when errors.Is or errors.As stops matching after a %v wrap; when a slice mutates its parent, a map iterates in a new order, or a typed nil returned as error compares non-nil; when go.mod, go.sum, workspaces, replace, or a /v2 path fight over dependencies; when cross-compiling, cgo, build tags, go:embed, or a static container binary fail; when tests are flaky under -race or -parallel or benchmarks lie; when GOGC, GOMEMLIMIT, GOMAXPROCS, or escape analysis need tuning; or when writing handlers, CLIs, database access, or generics. Not for Kubernetes operators or framework internals.
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.
fatal error:, or race warning whose cause is not obvious in ten seconds| 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.
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).%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).var e *MyErr = nil; var err error = e → err != 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).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).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).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).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).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.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).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 |
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 |
Before delivering Go code, check:
go statement has a stated exit path, and every WithCancel/WithTimeout has defer cancel()%w; every discarded error has _ = and a written reasonctx is the first parameter of every exported call that blocks, and every network, database, lock, and subprocess call has a deadlineerror; no typed nil escapes as an interfaceClose() errors on writers are checkedgo vet is clean and go test -race ./... ran at least once against the changed packagesmin_go, and the go directive in go.mod actually allows what you emittedtext/template for HTML, a concatenated SQL string, os/exec with a shell, or a path join without containment; no math/rand for anything secretUser-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:
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)deployment.md and modules.md guidancepanic in library code, unbounded goroutines, and unpinned dependencies — affects security.md and review depthpkg/errors vs stdlib, sqlx vs database/sql, ORM policy), tolerance for new transitive dependencies — affects every recommendationgo 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 rulesupgrade_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 rulesdefault_timeout — SetMaxOpenConns/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| 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) |
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).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).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).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).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's debugging.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 codePart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/go.