解决 Terraform 卡点:调试 plan/apply 失败、安全重构线上资源、精确钉住 provider 与模块版本。
记忆
Go
调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。
它能做什么
针对 Go 的具体故障——"concurrent map writes"、"send on closed channel"、数据竞争告警、卡顿、泄漏、nil 接口、`go.mod` 冲突等——映射到专门指南(concurrency.md、errors.md、interfaces.md、modules.md 等 20 余个)。每条建议都锚定在九条核心规则清单、`go >=X.Y` 版本下限表和 panic 应对动作对照表上。最后通过输出门禁检查 goroutine 退出路径、`%w` 错误包装、`defer cancel()` 和 nil 接口返回。明确不在范围内:Kubernetes operator、gRPC 服务设计和 Web 框架内部实现。
什么时候用它
- 诊断 `go test -race` 输出的 `WARNING: DATA RACE`
- 修复导致 RSS 持续上涨的 goroutine 或定时器泄漏
- 解决 `%v` 包装重构后 `errors.Is` 失效的问题
- 处理 `go.mod`/`go.sum`、replace 指令或 `/v2` 路径冲突
技能文档
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.
常见问题
- 用户数据存放在哪里?
- 仅存放在 `~/Clawic/data/go/config.yaml`。旧路径(`~/go/` 或 `~/clawic/go/`)应迁移到此;本技能不会写入或删除 `~/go/`,因为它可能同时是用户的 GOPATH。
- 是否覆盖 Kubernetes operator、gRPC 或 Web 框架内部实现?
- 不覆盖。这些主题由其他技能负责;本技能聚焦于 Go 语言、标准库与工具链(modules、build、test、vet、pprof、govulncheck)。
- 如何处理不同 Go 版本的差异?
- 每份指南都内联 `go >=X.Y` 版本下限,SKILL.md 提供从 1.13(`%w`、`errors.Is`/`As`)到 1.25(感知 cgroup 的 `GOMAXPROCS`、`testing/synctest`)的总表,建议严格匹配项目 `go.mod` 中的指令。
相关技能
按文档化的 Python 规则手册和固定检查清单排查、审阅与编写代码。
针对 Node.js 运行时、打包与线上问题,给出可落地的诊断依据与修复规则。
编写、审阅、调优 MySQL、SQLite、MariaDB、SQL Server 上的 SQL,并安全迁移。
以 kubectl 为先的分诊流程诊断 Kubernetes 工作负载故障并审查清单,判断全部落在本地笔记上。
围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。