Goroutines Channels
Concurrency patterns using goroutines, channels, select, and sync primitives in Go
You are an expert in Go concurrency, helping developers write correct, efficient concurrent code using goroutines, channels, and synchronization primitives. ## Key Points - Launched with the `go` keyword; cost roughly 2-8 KB of stack (grows as needed). - Multiplexed onto OS threads by the Go scheduler (M:N scheduling). - No return value — communicate results via channels or shared state with synchronization. - **Unbuffered**: `ch := make(chan int)` — sender blocks until receiver is ready. - **Buffered**: `ch := make(chan int, 10)` — sender blocks only when buffer is full. - **Directional**: `chan<- int` (send-only), `<-chan int` (receive-only) for API clarity. - Closing a channel signals "no more values"; receivers get zero-value after drain. - Multiplexes across multiple channel operations. - `default` case makes it non-blocking. - Always ensure every goroutine has a clear exit path; use `context.Context` or done channels. - Use `sync.WaitGroup` to wait for a group of goroutines to finish. - Prefer channels for communication, mutexes for protecting shared state.
skilldb get go-skills/Goroutines ChannelsFull skill: 176 linesGoroutines & Channels — Go Programming
You are an expert in Go concurrency, helping developers write correct, efficient concurrent code using goroutines, channels, and synchronization primitives.
Core Philosophy
Go's concurrency model is rooted in Tony Hoare's Communicating Sequential Processes: instead of sharing memory and protecting it with locks, concurrent processes should communicate by sending messages through channels. The famous Go proverb — "Don't communicate by sharing memory; share memory by communicating" — is not just a slogan but a design principle baked into the language. Goroutines are cheap enough to launch thousands of them, and channels provide the synchronization points where data flows safely between them.
The real art of Go concurrency is not in launching goroutines — that is trivially easy — but in ensuring every goroutine has a clear lifecycle. A goroutine without an exit condition is a resource leak. Before writing go func(), you should be able to answer three questions: What makes this goroutine start? What makes it stop? Who waits for it to finish? If you cannot answer all three, you are creating a goroutine leak that will grow unbounded in a long-running server.
Channels and mutexes are complementary tools, not competitors. Channels excel at transferring ownership of data between goroutines and orchestrating workflows (pipelines, fan-out, fan-in). Mutexes excel at protecting shared state that multiple goroutines read and write concurrently. Using a channel where a simple sync.Mutex would suffice makes code harder to follow; using a mutex where a channel pipeline would be clearer produces tangled, deadlock-prone code. Choose the tool that makes the concurrency structure of your program most obvious.
Anti-Patterns
-
Fire-and-forget goroutines: Launching
go doSomething()without any mechanism to wait for completion or detect failure means errors vanish silently and panics crash the process. Always pair goroutine launches with async.WaitGroup,errgroup.Group, or a done channel. -
Closing channels from the receiver side: The sender knows when there is no more data to send; the receiver does not. Closing a channel from the receiver risks a panic if the sender writes to the closed channel. Establish clear ownership: the sender closes, the receiver ranges.
-
Using unbuffered channels as queues: An unbuffered channel is a synchronization point, not a buffer. If your intent is to decouple producer speed from consumer speed, use a buffered channel with an explicit capacity. If your intent is to hand off work synchronously, unbuffered is correct — just be intentional about the choice.
-
Spawning a goroutine per request without bounds: In a server handling thousands of concurrent requests, launching an unbounded number of goroutines for background work can exhaust memory. Use a worker pool or a semaphore (
make(chan struct{}, maxConcurrency)) to limit concurrency. -
Sharing a
sync.WaitGroupby value:sync.WaitGroupcontains internal state that must not be copied. Passing it by value to a goroutine creates an independent copy, sowg.Done()decrements a different counter. Always pass*sync.WaitGroupor use it via closure.
Overview
Go's concurrency model is built on CSP (Communicating Sequential Processes). Goroutines are lightweight threads managed by the Go runtime, and channels are typed conduits for communication between them. Together with the sync package, they provide the building blocks for all concurrent Go programs.
Core Concepts
Goroutines
- Launched with the
gokeyword; cost roughly 2-8 KB of stack (grows as needed). - Multiplexed onto OS threads by the Go scheduler (M:N scheduling).
- No return value — communicate results via channels or shared state with synchronization.
Channels
- Unbuffered:
ch := make(chan int)— sender blocks until receiver is ready. - Buffered:
ch := make(chan int, 10)— sender blocks only when buffer is full. - Directional:
chan<- int(send-only),<-chan int(receive-only) for API clarity. - Closing a channel signals "no more values"; receivers get zero-value after drain.
Select
- Multiplexes across multiple channel operations.
defaultcase makes it non-blocking.
Implementation Patterns
Fan-out / Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
results := make(chan int)
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for v := range input {
results <- process(v)
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}
Pipeline
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// Usage: result := sq(sq(gen(2, 3, 4)))
Worker Pool
func workerPool(ctx context.Context, jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
results <- job.Process()
case <-ctx.Done():
return
}
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
Timeout with Select
select {
case res := <-ch:
fmt.Println("received", res)
case <-time.After(3 * time.Second):
fmt.Println("timed out")
}
Rate Limiting
limiter := time.NewTicker(200 * time.Millisecond)
defer limiter.Stop()
for req := range requests {
<-limiter.C
go handle(req)
}
Best Practices
- Always ensure every goroutine has a clear exit path; use
context.Contextor done channels. - Use
sync.WaitGroupto wait for a group of goroutines to finish. - Prefer channels for communication, mutexes for protecting shared state.
- Close channels from the sender side, never from the receiver.
- Use
sync.Oncefor one-time initialization. - Use
errgroup.Group(fromgolang.org/x/sync/errgroup) when goroutines can return errors.
Common Pitfalls
- Goroutine leaks: forgetting to cancel a context or close a channel leaves goroutines hanging forever.
- Race conditions: access shared variables without synchronization; use
go run -raceto detect. - Deadlocks: two goroutines waiting on each other; keep channel direction and ownership clear.
- Closing a closed channel: causes a panic; track ownership so only one goroutine closes.
- Loop variable capture: in
forloops launching goroutines, capture the loop variable explicitly (fixed in Go 1.22+ withloopvar). - Sending on a nil channel blocks forever: always initialize channels before use.
Install this skill directly: skilldb add go-skills
Related Skills
Context Patterns
Context usage for cancellation, timeouts, deadlines, and value propagation in Go
Error Handling
Error handling patterns including wrapping, sentinel errors, custom types, and error groups in Go
Generics
Go generics including type parameters, constraints, and generic data structure patterns (Go 1.18+)
HTTP Servers
HTTP server patterns using net/http, chi, and gin including middleware, routing, and graceful shutdown
Interfaces
Interface design principles, implicit satisfaction, and composition patterns in Go
Modules
Go modules, dependency management, versioning, workspaces, and private module configuration