Go's defining feature is native concurrency: "don't communicate by sharing memory; share memory by communicating". CSP pattern delivers highly readable, safe concurrent code.
Goroutines and Worker Pool Pattern
A goroutine starts with 2KB stack, so hundreds of thousands are practical. Bound concurrency with buffered channel for fixed worker pool; avoid runaway creation stressing scheduler. Use sync.WaitGroup to await graceful shutdown.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
)
type Job struct{ ID int; URL string }
type Result struct{ JobID int; Latency time.Duration; Status int; Err error }
func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
log.Printf("[worker %d] started", id)
for {
select {
case <-ctx.Done():
log.Printf("[worker %d] cancelled: %v", id, ctx.Err())
return
case job, ok := <-jobs:
if !ok { return }
t0 := time.Now()
select {
case <-ctx.Done():
results <- Result{JobID: job.ID, Err: ctx.Err()}
return
case <-time.After(500 * time.Millisecond):
results <- Result{JobID: job.ID, Latency: time.Since(t0), Status: 200}
}
}
}
}
func main() {
const (
workerCount = 4
jobCount = 20
)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jobs := make(chan Job, workerCount*2)
results := make(chan Result, workerCount*2)
var wg sync.WaitGroup
for i := 1; i <= workerCount; i++ {
wg.Add(1)
go worker(ctx, i, jobs, results, &wg)
}
go func() {
for i := 1; i <= jobCount; i++ {
jobs <- Job{ID: i, URL: fmt.Sprintf("https://api.example.com/item/%d", i)}
}
close(jobs)
}()
go func() { wg.Wait(); close(results) }()
total := 0
for r := range results {
total++
if r.Err != nil { fmt.Printf("[job %02d] ERROR %v\n", r.JobID, r.Err) }
fmt.Printf("[job %02d] status=%d latency=%-8s total=%d\n", r.JobID, r.Status, r.Latency, total)
}
fmt.Printf("Done: %d/%d jobs\n", total, jobCount)
}
Context Cancellation and Pitfalls
context.WithTimeout/WithCancel propagates cancellation through all nested goroutines - key defense against leaks. Common traps: for-range on closed channels exit cleanly, select with default causes busy loops, unbuffered channel send blocks and hangs goroutines.
| Feature | Go goroutine+channel | Java Thread+Queue | Rust async+tokio |
|---|---|---|---|
| Min cost | ~2KB stack | ~1MB stack | Struct-level |
| Spawn 100k time | <100ms | Seconds/OOM | ~200ms |
| Sync primitives | channel/mutex/Cond | BlockingQueue/locks | tokio::sync::* |
| Cancel mechanism | Unified Context | Future.cancel/interrupt | Drop/select! |
| Pitfalls | Goroutine leaks/blocked | Spurious wakeups/deadlock | Pin! lifetimes |
Best Practices
Senders close channels, not receivers. Prefer bounded buffered channels to avoid unbounded memory growth. Cover all exit paths with defer for cleanup; exceptions otherwise silently leak goroutines.