Golang Concurrency
Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe
- Skill
- Guidelines
- Software development
- MIT
- Low risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive framework for designing and auditing concurrent Go applications. It emphasizes structured concurrency, focusing on goroutine lifecycle management, leak prevention, and the correct application of synchronization primitives like channels, mutexes, and atomics. The skill includes specific patterns for worker pools, fan-out/fan-in pipelines, and error propagation using errgroup, while providing a systematic approach to auditing codebases for concurrency issues.
What it does
- Provides design patterns for goroutine lifecycle management and clean shutdown.
- Offers guidance on choosing between channels, mutexes, atomics, and sync primitives.
- Defines best practices for implementing worker pools and pipeline architectures.
- Includes a structured checklist for reviewing concurrent code and identifying potential leaks.
- Supports multi-agent orchestration for auditing concurrency across large codebases.
- References standard synchronization tools like errgroup, singleflight, and sync.Map.
When to use it
- When writing new concurrent Go code that requires robust lifecycle management.
- When reviewing pull requests to identify goroutine leaks or ownership violations.
- When auditing an existing codebase for unprotected shared state or synchronization errors.
- When determining the appropriate concurrency primitive for a specific synchronization task.
Advantages
- Promotes structured concurrency to prevent goroutine leaks and deadlocks.
- Standardizes the use of synchronization primitives across development teams.
- Facilitates systematic auditing of complex concurrent systems.
- Provides clear, actionable patterns for common concurrency challenges.
Usage examples
- “Review this PR for goroutine leaks and synchronization issues.”
- The agent analyzes the diff, checking for missing context propagation, improper channel closure, and unprotected shared state.
- “Help me implement a worker pool for processing tasks.”
- The agent provides a pattern using errgroup.SetLimit to manage bounded concurrency and ensure clean error handling.
- “Audit the codebase for potential concurrency bugs.”
- The agent orchestrates multiple sub-agents to scan for mutable globals, improper channel usage, and missing context cancellation.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/samber/golang-concurrency/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip golang-concurrency.zip -d .agents/skills
Documentation by samber
Persona: You are a Go concurrency engineer. You assume every goroutine is a liability until proven necessary — correctness and leak-freedom come before performance.
Orchestration mode: Fan out the five sub-agents described in the "Parallelizing Concurrency Audits" section for auditing concurrent code across a large codebase, and consolidate their findings into one report. On Claude Code, use ultracode to opt into multi-agent orchestration explicitly.
Modes:
Write mode — implement concurrent code (goroutines, channels, sync primitives, worker pools, pipelines). Follow the sequential instructions below.
Review mode — reviewing a PR's concurrent code changes. Focus on the diff: check for goroutine leaks, missing context propagation, ownership violations, and unprotected shared state. Sequential.
Audit mode — auditing existing concurrent code across a codebase. Use up to 5 parallel sub-agents as described in the "Parallelizing Concurrency Audits" section.
Community default. A company skill that explicitly supersedes
samber/cc-skills-golang@golang-concurrencyskill takes precedence.
Go Concurrency Best Practices
Go's concurrency model is built on goroutines and channels. Goroutines are cheap but not free — every goroutine you spawn is a resource you must manage. The goal is structured concurrency: every goroutine has a clear owner, a predictable exit, and proper error propagation.
Core Principles
Every goroutine must have a clear exit — without a shutdown mechanism (context, done channel, WaitGroup), they leak and accumulate until the process crashes
Share memory by communicating — channels transfer ownership explicitly; mutexes protect shared state but make ownership implicit
Send copies, not pointers on channels — sending pointers creates invisible shared memory, defeating the purpose of channels
Only the sender closes a channel — closing from the receiver side panics if the sender writes after close
Specify channel direction (
chan<-,<-chan) — the compiler prevents misuse at build timeDefault to unbuffered channels — larger buffers mask backpressure; use them only with measured justification
Always include
ctx.Done()in select — without it, goroutines leak after caller cancellationAvoid repeated
time.Afterin hot loops — each call allocates a timer and creates unnecessary churn; usetime.NewTimer+Resetfor long-running loopsTrack goroutine leaks in tests with
go.uber.org/goleak
For detailed channel/select code examples, see Channels and Select Patterns.
Channel vs Mutex vs Atomic
| Scenario | Use | Why |
|---|---|---|
| Passing data between goroutines | Channel | Communicates ownership transfer |
| Coordinating goroutine lifecycle | Channel + context | Clean shutdown with select |
| Protecting shared struct fields | sync.Mutex / sync.RWMutex | Simple critical sections |
| Simple counters, flags | sync/atomic | Lock-free, lower overhead |
| Many readers, few writers on a map | sync.Map | Optimized for read-heavy workloads. Concurrent map read/write causes a hard crash |
| Caching expensive computations | sync.Once / singleflight | Execute once or deduplicate |
WaitGroup vs errgroup
| Need | Use | Why |
|---|---|---|
| Wait for goroutines, errors not needed | sync.WaitGroup | Fire-and-forget |
| Wait + collect first error | errgroup.Group | Error propagation |
| Wait + cancel siblings on first error | errgroup.WithContext | Context cancellation on error |
| Wait + limit concurrency | errgroup.SetLimit(n) | Built-in worker pool |
Sync Primitives Quick Reference
| Primitive | Use case | Key notes |
|---|---|---|
sync.Mutex | Protect shared state | Keep critical sections short; never hold across I/O |
sync.RWMutex | Many readers, few writers | Never upgrade RLock to Lock (deadlock) |
sync/atomic | Simple counters, flags | Prefer typed atomics (Go 1.19+): atomic.Int64, atomic.Bool |
sync.Map | Concurrent map, read-heavy | No explicit locking; use RWMutex+map when writes dominate |
sync.Pool | Reuse temporary objects | Always Reset() before Put(); reduces GC pressure |
sync.Once | One-time initialization | Go 1.21+: OnceFunc, OnceValue, OnceValues |
sync.WaitGroup | Waiting for simple goroutines | Go 1.25+: prefer wg.Go(func(){ ... }) for fire-and-wait tasks that do not panic and do not need error propagation. For Go <1.25 use Add/Done. For errors/cancellation/limits, use errgroup with context. |
x/sync/singleflight | Deduplicate concurrent calls | Cache stampede prevention |
x/sync/errgroup | Goroutine group + errors | SetLimit(n) replaces hand-rolled worker pools |
For detailed examples and anti-patterns, see Sync Primitives Deep Dive.
Concurrency Checklist
Before spawning a goroutine, answer:
[ ] How will it exit? — context cancellation, channel close, or explicit signal
[ ] Can I signal it to stop? — pass
context.Contextor done channel[ ] Can I wait for it? —
sync.WaitGrouporerrgroup[ ] Who owns the channels? — creator/sender owns and closes
[ ] Should this be synchronous instead? — don't add concurrency without measured need
Pipelines and Worker Pools
For pipeline patterns (fan-out/fan-in, bounded workers, generator chains, Go 1.23+ iterators, samber/ro), see Pipelines and Worker Pools.
Parallelizing Concurrency Audits
When auditing concurrency across a large codebase, use up to 5 parallel sub-agents:
Find all goroutine spawns (
go func,go method) and verify shutdown mechanismsSearch for mutable globals and shared state without synchronization
Audit channel usage — ownership, direction, closure, buffer sizes
Find
time.Afterin loops, missingctx.Done()in select, unbounded spawningCheck mutex usage,
sync.Map, atomics, and thread-safety documentation
Common Mistakes
| Mistake | Fix |
|---|---|
| Fire-and-forget goroutine | Provide stop mechanism (context, done channel) |
| Closing channel from receiver | Only the sender closes |
time.After in hot loop | Reuse time.NewTimer + Reset |
Missing ctx.Done() in select | Always select on context to allow cancellation |
| Unbounded goroutine spawning | Use errgroup.SetLimit(n) or semaphore |
| Sharing pointer via channel | Send copies or immutable values |
wg.Add inside goroutine | Call Add before go — Wait may return early otherwise |
Forgetting -race in CI | Always run go test -race ./... |
| Mutex held across I/O | Keep critical sections short |
Cross-References
→ See
samber/cc-skills-golang@golang-performanceskill for false sharing, cache-line padding,sync.Poolhot-path patterns→ See
samber/cc-skills-golang@golang-contextskill for cancellation propagation and timeout patterns→ See
samber/cc-skills-golang@golang-safetyskill for concurrent map access and race condition prevention→ See
samber/cc-skills-golang@golang-troubleshootingskill for debugging goroutine leaks and deadlocks→ See
samber/cc-skills-golang@golang-design-patternsskill for graceful shutdown patterns→ See
samber/cc-skills-golang@golang-continuous-integrationskill for automated AI-driven code review in CI using these guidelines
Goroutine leak profile
The goroutine leak profile (experimental behind GOEXPERIMENT=goroutineleakprofile in Go 1.26) is generally available in runtime/pprof since Go 1.27 — no build flag required. It is a useful production-oriented leak signal alongside the existing tools below.
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleakKeep existing tools:
tests:
go.uber.org/goleakruntime count:
runtime.NumGoroutine()stack dump:
/debug/pprof/goroutine?debug=2race checks:
go test -race ./...
References
Files
- ATTRIBUTION.mdAdded by AutoGineer911 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md9.3 KiB
- evals/evals.json16.1 KiB
- references/channels-and-select.md4.3 KiB
- references/pipelines.md7.2 KiB
- references/sync-primitives.md8.6 KiB
evals/evals.json
[
{
"id": 1,
"name": "time-after-in-select-loop",
"description": "Tests whether the model avoids time.After inside a select loop and uses time.NewTimer with Reset instead",
"prompt": "I have a Go worker that reads from a channel and needs a 5-second inactivity timeout. If nothing arrives for 5 seconds, it should log and continue waiting. Write the select loop.",
"trap": "Without the skill, the model uses time.After(5*time.Second) inside the for/select loop, creating repeated timer allocations/churn instead of reusing a timer",
"assertions": [
{ "id": "1.1", "text": "Does NOT use time.After inside the loop body" },
{
"id": "1.2",
"text": "Creates time.NewTimer (or time.NewTicker) outside the loop"
},
{
"id": "1.3",
"text": "Calls timer.Reset() after handling a message or timeout"
},
{
"id": "1.4",
"text": "Calls timer.Stop() (or defers it) to clean up the timer"
},
{
"id": "1.5",
"text": "For Go 1.23+, does not use the old stale-drain pattern; for Go <1.23 compatibility, drains before Reset when Stop reports a possible pending value"
}
]
},
{
"id": 2,
"name": "channel-closing-ownership",
"description": "Tests whether the model follows the rule that only the sender closes a channel, not the receiver",
"prompt": "I have a producer goroutine that sends items to a channel and a consumer goroutine that reads from it. The consumer knows when it has received enough items and wants to signal completion. How should I close the channel to stop the producer?",
"trap": "Without the skill, the model closes the channel from the consumer side, which panics if the producer writes after close",
"assertions": [
{
"id": "2.1",
"text": "Does NOT close the channel from the consumer/receiver side"
},
{
"id": "2.2",
"text": "Uses a separate signaling mechanism (done channel, context cancellation, or similar) for the consumer to tell the producer to stop"
},
{
"id": "2.3",
"text": "The producer is the one that closes the data channel (or it is closed by the channel creator/sender)"
},
{
"id": "2.4",
"text": "Explains the panic risk of closing a channel from the receiver side"
},
{
"id": "2.5",
"text": "The producer selects on the stop signal alongside its send operation"
}
]
},
{
"id": 3,
"name": "waitgroup-add-placement",
"description": "wg.Add before go statement; wg.Add(len(urls)) is preferred over per-goroutine Add(1) when count is known",
"prompt": "Review this Go code and identify all WaitGroup issues:\n\n```go\nfunc ProcessURLs(urls []string) []Result {\n var wg sync.WaitGroup\n results := make([]Result, len(urls))\n\n for i, url := range urls {\n go func(i int, url string) {\n wg.Add(1)\n defer wg.Done()\n results[i] = fetch(url)\n }(i, url)\n }\n\n wg.Wait()\n return results\n}\n```\n\nFix all issues. Also, is there a more efficient way to add to the WaitGroup when the number of goroutines is known upfront?",
"trap": "The most obvious bug (wg.Add inside goroutine) is well-known. The model should also catch the second issue: wg.Add(len(urls)) before the loop is more efficient than N separate wg.Add(1) calls (single atomic update vs N atomic updates). The model likely fixes the placement but may not suggest the batch Add optimization.",
"assertions": [
{
"id": "3.1",
"text": "Moves wg.Add(1) BEFORE the go statement — wg.Add inside the goroutine races with wg.Wait(), which can return before all goroutines have registered"
},
{
"id": "3.2",
"text": "Suggests using wg.Add(len(urls)) before the loop as a more efficient alternative — a single atomic operation instead of N separate wg.Add(1) calls in the loop"
},
{ "id": "3.3", "text": "Keeps defer wg.Done() inside the goroutine" },
{
"id": "3.4",
"text": "Notes the race condition: wg.Wait() could return before some goroutines have called wg.Add(1), causing ProcessURLs to return with goroutines still running"
}
]
},
{
"id": 4,
"name": "channel-direction-in-signatures",
"description": "Tests whether the model specifies channel direction (send-only, receive-only) in function signatures",
"prompt": "Write a Go pipeline with two functions: one that generates integers from a slice and sends them to a channel, and one that reads integers from a channel, doubles them, and sends to an output channel. Wire them together in main.",
"trap": "Without the skill, the model uses bidirectional chan int in function parameters instead of chan<- and <-chan",
"assertions": [
{
"id": "4.1",
"text": "The generator function returns <-chan int (receive-only for callers)"
},
{
"id": "4.2",
"text": "The doubler function accepts <-chan int as input parameter"
},
{
"id": "4.3",
"text": "The doubler function returns <-chan int (receive-only for callers)"
},
{
"id": "4.4",
"text": "Internally, channels are created as bidirectional but exposed as directional through return types"
},
{
"id": "4.5",
"text": "The producer (generator) closes its output channel with defer close(out)"
}
]
},
{
"id": 5,
"name": "unbuffered-channel-default",
"description": "Tests whether the model defaults to unbuffered channels and requires justification for buffered ones",
"prompt": "I need a channel to pass tasks from a dispatcher to a pool of workers in Go. Should I buffer it? If so, how large?",
"trap": "Without the skill, the model suggests an arbitrary large buffer (e.g., 100 or 1000) without justification or discussion of backpressure",
"assertions": [
{
"id": "5.1",
"text": "Recommends starting with unbuffered (or very small buffer like 0 or 1) as the default"
},
{
"id": "5.2",
"text": "Explains that large buffers mask backpressure problems"
},
{
"id": "5.3",
"text": "States that buffer size should be based on measured need, not arbitrary choice"
},
{
"id": "5.4",
"text": "Does NOT suggest a large arbitrary buffer (e.g., 100, 1000) without explaining the tradeoffs"
},
{
"id": "5.5",
"text": "Mentions that buffered channels hide the problem of slow consumers"
}
]
},
{
"id": 6,
"name": "send-copies-not-pointers",
"description": "Tests whether the model sends copies (not pointers) through channels to avoid shared memory",
"prompt": "I have a producer goroutine that creates Task structs and sends them to a channel for worker goroutines to process. The Task struct has fields like ID, Payload, and Status. Write the producer and consumer code.",
"trap": "Without the skill, the model sends *Task pointers through the channel, creating invisible shared memory that defeats the purpose of channels",
"assertions": [
{
"id": "6.1",
"text": "Sends Task values (not *Task pointers) through the channel, OR explicitly documents why pointers are safe in this case"
},
{
"id": "6.2",
"text": "The channel type is chan Task (value type) rather than chan *Task"
},
{
"id": "6.3",
"text": "Does not mutate the Task struct after sending it on the channel (or sends a copy)"
},
{
"id": "6.4",
"text": "If pointers are used, explicitly acknowledges the shared-memory risk and explains the mitigation"
}
]
},
{
"id": 7,
"name": "errgroup-vs-waitgroup-decision",
"description": "Tests whether the model chooses errgroup over WaitGroup when error handling is needed, and uses SetLimit for bounded concurrency",
"prompt": "Write a Go function that fetches data from 50 URLs concurrently. At most 10 should run at once. If any fetch fails, stop all remaining work and return the error.",
"trap": "Without the skill, the model builds a hand-rolled worker pool with WaitGroup + semaphore channel, missing errgroup.SetLimit which handles this natively",
"assertions": [
{
"id": "7.1",
"text": "Uses errgroup (golang.org/x/sync/errgroup) instead of sync.WaitGroup for error propagation"
},
{
"id": "7.2",
"text": "Uses errgroup.WithContext to cancel siblings on first error"
},
{
"id": "7.3",
"text": "Uses g.SetLimit(10) for bounded concurrency instead of a hand-rolled semaphore"
},
{
"id": "7.4",
"text": "Does NOT build a manual worker pool with channels and WaitGroup when errgroup suffices"
},
{
"id": "7.5",
"text": "Each goroutine checks ctx.Done() or uses the context from errgroup.WithContext"
}
]
},
{
"id": 8,
"name": "ctx-done-in-select",
"description": "Tests whether every select statement includes a ctx.Done() case to prevent goroutine leaks",
"prompt": "Write a Go function that reads from an input channel, transforms each item, and writes to an output channel. It should run as a goroutine.",
"trap": "Without the skill, the model writes a select with only channel operations but no ctx.Done() case, causing goroutine leaks on cancellation",
"assertions": [
{
"id": "8.1",
"text": "The function accepts a context.Context parameter"
},
{
"id": "8.2",
"text": "Every select statement includes a case <-ctx.Done(): return branch"
},
{
"id": "8.3",
"text": "Both the read from input channel AND the write to output channel are wrapped in select with ctx.Done()"
},
{
"id": "8.4",
"text": "The goroutine exits cleanly when context is cancelled"
},
{
"id": "8.5",
"text": "The output channel is closed when the goroutine exits (defer close)"
}
]
},
{
"id": 9,
"name": "sync-map-vs-rwmutex-decision",
"description": "Tests whether the model correctly chooses between sync.Map and RWMutex+map based on access patterns",
"prompt": "I need a concurrent cache in Go. Keys are strings, values are structs. The cache will have frequent writes from many goroutines updating the same keys, and reads happen about as often as writes. Which synchronization approach should I use?",
"trap": "Without the skill, the model recommends sync.Map because it sounds like 'concurrent map', but sync.Map is slower for write-heavy overlapping-key patterns",
"assertions": [
{
"id": "9.1",
"text": "Recommends sync.RWMutex + plain map over sync.Map for this write-heavy, overlapping-key pattern"
},
{
"id": "9.2",
"text": "Explains that sync.Map is optimized for write-once/read-many or disjoint key sets"
},
{
"id": "9.3",
"text": "Explains that for frequent writes with overlapping keys, RWMutex+map is faster"
},
{
"id": "9.4",
"text": "Does NOT unconditionally recommend sync.Map for any concurrent map scenario"
},
{
"id": "9.5",
"text": "Mentions that concurrent map read/write without synchronization causes a hard crash (not just a data race)"
}
]
},
{
"id": 10,
"name": "sync-pool-reset-before-put",
"description": "Tests whether the model resets objects before returning them to sync.Pool",
"prompt": "Write a Go function that uses sync.Pool to reuse bytes.Buffer objects for encoding JSON responses in an HTTP handler. Show the pool setup and the handler.",
"trap": "Without the skill, the model returns the buffer to the pool without calling Reset(), leading to dirty objects and data corruption",
"assertions": [
{
"id": "10.1",
"text": "Calls buf.Reset() BEFORE bufPool.Put(buf), not after Get()"
},
{
"id": "10.2",
"text": "Uses defer to ensure the buffer is returned to the pool even on error"
},
{
"id": "10.3",
"text": "Does not assume the object from Get() is clean/zeroed"
},
{ "id": "10.4", "text": "The pool's New function creates a new buffer" },
{
"id": "10.5",
"text": "Does not store persistent state in pooled objects"
}
]
},
{
"id": 11,
"name": "goroutine-panic-recovery",
"description": "Tests whether the model adds panic recovery at goroutine boundaries in production code",
"prompt": "Write a Go HTTP server that spawns a background goroutine per request to do async processing (e.g., sending a notification email). The main handler returns 202 Accepted immediately.",
"trap": "Without the skill, the model spawns go func() without recover(), so a panic in the background goroutine crashes the entire server process",
"assertions": [
{
"id": "11.1",
"text": "Adds defer func() { recover() }() or equivalent panic recovery inside the goroutine"
},
{
"id": "11.2",
"text": "Logs or handles the recovered panic (not just silently swallowed)"
},
{
"id": "11.3",
"text": "The goroutine has a shutdown mechanism (context, done channel, or similar)"
},
{
"id": "11.4",
"text": "Mentions that a panic in a goroutine crashes the entire process"
}
]
},
{
"id": 12,
"name": "singleflight-cache-stampede",
"description": "Tests whether the model uses singleflight to deduplicate concurrent requests for the same resource",
"prompt": "My Go service has an expensive database query for user profiles. Under load, hundreds of goroutines request the same user profile simultaneously, causing a thundering herd on the database. How should I solve this?",
"trap": "Without the skill, the model suggests only traditional caching (TTL-based) or a mutex, missing singleflight which deduplicates in-flight requests",
"assertions": [
{
"id": "12.1",
"text": "Recommends golang.org/x/sync/singleflight as the primary solution"
},
{
"id": "12.2",
"text": "Shows usage of group.Do(key, func) where key identifies the deduplicated resource"
},
{
"id": "12.3",
"text": "Explains that only one goroutine executes the function; others wait and share the result"
},
{
"id": "12.4",
"text": "May combine singleflight with a cache layer for TTL-based caching, but singleflight is the deduplication mechanism"
},
{
"id": "12.5",
"text": "Does NOT suggest only a plain mutex or only a TTL cache as the solution to thundering herd"
}
]
},
{
"id": 13,
"name": "iterator-vs-goroutine-pipeline",
"description": "Tests whether the model uses Go 1.23+ iterators for sequential CPU-bound transforms instead of goroutine+channel pipelines",
"prompt": "I have a Go 1.23+ project. I need to filter a slice of users to find active ones, then extract their email addresses. The slice is in-memory and processing is pure CPU. Should I use a goroutine pipeline with channels?",
"trap": "Without the skill, the model builds a goroutine+channel pipeline for a purely sequential, CPU-bound in-memory transformation where iterators are more appropriate",
"assertions": [
{
"id": "13.1",
"text": "Recommends against goroutine+channel pipeline for this purely sequential, in-memory transform"
},
{
"id": "13.2",
"text": "Suggests Go 1.23+ iterators (iter.Seq) or simple slice operations instead"
},
{
"id": "13.3",
"text": "Explains that goroutine+channel pipelines add overhead without benefit for sequential CPU-bound work"
},
{
"id": "13.4",
"text": "Mentions that goroutine pipelines are appropriate when stages involve I/O or need true parallelism"
},
{
"id": "13.5",
"text": "Does NOT build a multi-goroutine pipeline for this use case"
}
]
}
]
Requirements and compatibility
Written for: Claude Code, OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Claude Code frontmatter, Needs network access
The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
Security
What our scanner noticed· Low risk
- Instructions that use the network
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- samber
- Publisher
- Samuel Berthe (Community)
- Distributed by
- Samuel Berthe
- Source
- samber/cc-skills-golang · skills/golang-concurrency · commit bb1d43d
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-concurrency.zip