原始内容
name: Go
slug: go
version: 1.0.4
changelog: "Display name shown correctly"
description: >-
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 - 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.
homepage: https://clawic.com/skills/go
metadata:
clawdbot:
emoji: 🐹
requires:
bins:
- go
os:
- linux
- darwin
- win32
displayName: Go
configPaths:
- ~/Clawic/data/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
| 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.