← GophersCONTENT HISTORY

Update to Gophers

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use when writing or reviewing concurrent Go code — goroutines, channels, select, mutexes, atomics, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Apply proactively whenever a goroutine is spawned, a shared field is mutated, or a channel is created, even if the user has not asked about concurrency. Does not cover context.Context patterns (see go-context).",
  "included_files": [
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 208
    },
    {
      "relative_path": "references/channels-and-select.md",
      "size_in_bytes": 3394
    },
    {
      "relative_path": "references/errgroup-and-pools.md",
      "size_in_bytes": 3675
    },
    {
      "relative_path": "references/leaks-and-synctest.md",
      "size_in_bytes": 3714
    },
    {
      "relative_path": "references/sync-primitives.md",
      "size_in_bytes": 3550
    }
  ],
  "name": "go-concurrency",
  "skill_md_contents": "---\nname: go-concurrency\ndescription: \"Use when writing or reviewing concurrent Go code — goroutines, channels, select, mutexes, atomics, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Apply proactively whenever a goroutine is spawned, a shared field is mutated, or a channel is created, even if the user has not asked about concurrency. Does not cover context.Context patterns (see go-context).\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Targets Go 1.21+ for typed atomics and slog. Notes Go 1.25 wg.Go, Go 1.26 testing/synctest, and Go 1.26 experimental goroutineleak profile where relevant.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go Concurrency\n\nGoroutines are cheap, but every one you spawn is a resource you must own. The goal is **structured concurrency**: each goroutine has a clear owner, a predictable exit, and a way for the caller to wait and collect errors.\n\n## Core Rules\n\n1. **Never start a goroutine without knowing how it will stop.** A blocked goroutine is not garbage-collected — it leaks.\n2. **The caller must be able to wait.** Use `sync.WaitGroup`, `errgroup.Group`, or an explicit done channel.\n3. **No goroutines in `init()`.** Expose `Start`/`Stop`/`Shutdown` so callers control the lifecycle.\n4. **Share by communicating.** Default to channels; reach for `sync.Mutex` only when the problem is genuinely \"protect a shared field\".\n5. **Only the sender closes a channel.** Closing from the receiver side panics on the next send.\n6. **Specify channel direction** (`chan<-`, `<-chan`) at function boundaries — the compiler catches misuse.\n7. **Always include `ctx.Done()` in `select`.** Without it, the goroutine cannot be cancelled.\n8. **Test for leaks** with [`go.uber.org/goleak`](https://pkg.go.dev/go.uber.org/goleak).\n\n## Primitive Decision\n\n| Need | Use | Why |\n|---|---|---|\n| Pass a value from producer to consumer | Channel | Transfers ownership explicitly |\n| Wait for N fire-and-forget goroutines | `sync.WaitGroup` (Go 1.25: `wg.Go`) | No error needed |\n| Wait + collect first error + cancel siblings | `errgroup.WithContext` | Structured failure |\n| Bound concurrency (worker pool) | `errgroup.SetLimit(n)` | Replaces hand-rolled pools |\n| Protect a shared field | `sync.Mutex` / `sync.RWMutex` | Short critical section |\n| Counter / flag | typed `sync/atomic` (`atomic.Int64`, `atomic.Bool`) | Lock-free, type-safe |\n| Read-heavy concurrent map | `sync.Map` | Concurrent map read/write otherwise crashes |\n| One-shot init | `sync.Once` (or `OnceFunc`/`OnceValue` in 1.21+) | Idempotent setup |\n| Deduplicate concurrent calls | `x/sync/singleflight` | Cache stampede prevention |\n\n> Read [references/sync-primitives.md](references/sync-primitives.md) when picking between mutex, atomic, `sync.Map`, `sync.Pool`, or `singleflight`, or when designing the field layout of a struct that protects shared state.\n\n## Goroutine Lifetimes\n\n```go\n// Good: bounded WaitGroup, deterministic exit\nvar wg sync.WaitGroup\nfor item := range queue {\n    wg.Add(1)\n    go func(it Item) { defer wg.Done(); process(ctx, it) }(item)\n}\nwg.Wait()\n\n// Bad: no stop signal, no wait — classic leak\ngo func() { for { flush(); time.Sleep(delay) } }()\n```\n\nGo 1.25+ exposes `wg.Go(fn)` which folds `Add`/`Done` into one call. Always call `wg.Add` **before** `go` — otherwise `wg.Wait` may return before the goroutine even starts.\n\n## errgroup: Errors and Cancellation\n\n`errgroup.WithContext` is the right default when sibling goroutines should cancel each other on the first failure:\n\n```go\ng, ctx := errgroup.WithContext(ctx)\ng.SetLimit(8)\nfor _, url := range urls {\n    g.Go(func() error { return fetch(ctx, url) })\n}\nif err := g.Wait(); err != nil {\n    return fmt.Errorf(\"fetching urls: %w\", err)\n}\n```\n\n`g.Wait` returns the first non-nil error; `ctx` is cancelled as soon as any worker fails. See [references/errgroup-and-pools.md](references/errgroup-and-pools.md).\n\n## Channels\n\n```go\nfunc produce(out chan<- int)                  { /* send-only */ }\nfunc consume(in <-chan int)                   { /* receive-only */ }\nfunc transform(in <-chan int, out chan<- int) { /* ownership crosses */ }\n```\n\n**Buffer size is `0` or `1`.** Anything larger must be justified (what bounds it under load, what happens when writers block).\n\nIn every long-running `select`, include `<-ctx.Done()`. Avoid `time.After` in hot loops — it allocates a timer per iteration; hoist a `time.NewTimer` and `Reset` it instead.\n\n> Read [references/channels-and-select.md](references/channels-and-select.md) when implementing pipelines, fan-in/fan-out, broadcast via `close`, or non-blocking sends with `default`.\n\n## Mutexes and Atomics\n\nThe zero value of `sync.Mutex`/`RWMutex` is valid — almost never use a pointer. Do not embed mutexes; keep them as an unexported `mu` field so `Lock`/`Unlock` aren't public API. Keep critical sections short; never hold a lock across I/O. Prefer typed atomics (`atomic.Bool`, `atomic.Int64`) over raw `sync/atomic` on `int32`/`int64` fields.\n\n## Testing: goleak and synctest\n\nWire `go.uber.org/goleak` into every package that spawns goroutines (`goleak.VerifyTestMain(m)` or `defer goleak.VerifyNone(t)`). For timer-dependent tests, use `testing/synctest` (Go 1.25+) so synthetic time advances deterministically. Go 1.26 adds an experimental `goroutineleak` pprof profile for production diagnosis — it is not a substitute for `goleak` in tests. See [references/leaks-and-synctest.md](references/leaks-and-synctest.md).\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| Fire-and-forget `go func()` with no signal | Leaks on shutdown; can outlive its inputs | Pass `ctx`, use `errgroup`, or own a done channel |\n| Closing a channel from the receiver | Panics on the next send | Only the sender closes |\n| `time.After` in a hot loop | Allocates a timer per iteration | `time.NewTimer` + `Reset` |\n| `select` without `ctx.Done()` | Cannot be cancelled | Always include the cancel case |\n| `wg.Add(1)` inside the goroutine | `Wait` may return before `Add` runs | `Add` before `go`, or use `wg.Go` (Go 1.25+) |\n| Buffered channel sized \"to be safe\" | Hides backpressure, masks bugs | Size 0 or 1; justify anything larger |\n| Concurrent read+write on `map` | Hard runtime crash, not a race warning | `sync.Map` or `sync.RWMutex` + map |\n| Mutex held across I/O / RPC | Serializes the whole service | Copy what you need under the lock; release before the call |\n| Sending a pointer through a channel | Re-introduces shared memory | Send a copy or an immutable value |\n| Forgetting `-race` in CI | Races ship to prod | `go test -race ./...` always |\n\n## Verification Checklist\n\nBefore finishing a concurrency change:\n\n- [ ] Every `go` has a documented exit (ctx, done channel, or bounded loop)\n- [ ] Every long-running `select` has a `<-ctx.Done()` case\n- [ ] `wg.Add` is called before `go`, or `wg.Go` is used (Go 1.25+)\n- [ ] Channels are sized 0 or 1, or the size has a comment justifying it\n- [ ] Only the sender closes channels; receivers use `for v := range ch` or `v, ok := <-ch`\n- [ ] No mutex is held across network/disk I/O\n- [ ] `go test -race ./...` is clean\n- [ ] Packages that spawn goroutines wire `goleak.VerifyTestMain` or per-test `VerifyNone`\n\n## References\n\n- [references/sync-primitives.md](references/sync-primitives.md) — mutex vs atomic vs `sync.Map`/`Pool`/`Once`/`singleflight`\n- [references/channels-and-select.md](references/channels-and-select.md) — channel ownership, direction, pipelines, non-blocking sends\n- [references/errgroup-and-pools.md](references/errgroup-and-pools.md) — `errgroup`, `SetLimit`, worker pools, fan-out/fan-in\n- [references/leaks-and-synctest.md](references/leaks-and-synctest.md) — `goleak`, `testing/synctest`, Go 1.26 experimental leak profile\n"
}

SHA-256 of public snapshot: 628b3cee629ae03e5f69bd7b30b7b82a08739f759486bdb39d1d2b63840b62b2