Go Concurrency
WaitGroup Β· Mutex & RWMutex Β· select Β· Context Β· Worker pools Β· Channel patterns Β· Once & atomic
Sheet 2 of 2
Go 1.22+
Intermediate
Printable
Goroutines β Deep Dive
Launch & Anonymous Goroutine
// Named function goroutine func worker(id int) { fmt.Printf("worker %d\n", id) } go worker(1) // Anonymous goroutine go func(id int) { fmt.Printf("anon %d\n", id) }(42) // call immediately
sync.WaitGroup β wait for all
import "sync" var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) // count up go func(n int) { defer wg.Done() // count down worker(n) }(i) } wg.Wait() // block until all Done()
GOMAXPROCS & runtime
import "runtime" // Number of OS threads for goroutines runtime.GOMAXPROCS(4) // Get CPU count cpus := runtime.NumCPU() runtime.GOMAXPROCS(cpus) // Active goroutines count n := runtime.NumGoroutine() // Yield to other goroutines runtime.Gosched()
Always pass loop variables into goroutines as arguments. Closures capture by reference β by the time the goroutine runs, the loop variable may have changed.
go func(n int){ ... }(i) is safe; go func(){ use(i) }() is a data race.Mutex & RWMutex
sync.Mutex β exclusive lock
type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.count++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count }
sync.RWMutex β concurrent reads
type Cache struct { mu sync.RWMutex data map[string]string } // Many goroutines can RLock at once func (c *Cache) Get(k string) string { c.mu.RLock() defer c.mu.RUnlock() return c.data[k] } // Only one goroutine can Lock at once func (c *Cache) Set(k, v string) { c.mu.Lock() defer c.mu.Unlock() c.data[k] = v }
Always defer Unlock. If you forget to unlock, every other goroutine waiting on the lock will block forever β a deadlock.
Channels β Patterns
Unbuffered vs Buffered
// Unbuffered β synchronous handoff ch := make(chan int) // Buffered β async up to capacity bch := make(chan int, 10) // Directional channels var send chan<- int // send-only var recv <-chan int // receive-only // Iterate until channel closes for v := range ch { fmt.Println(v) }
Done / Signal Channel Pattern
// Use struct{} β zero memory cost done := make(chan struct{}) go func() { // ... do work ... close(done) // signal finished }() <-done // block until closed // Check if closed without blocking select { case <-done: fmt.Println("finished") default: fmt.Println("still running") }
Only the sender should close a channel. Sending on a closed channel panics. Receiving from a closed channel returns the zero value immediately.
select Statement
Basic select
ch1 := make(chan string) ch2 := make(chan string) // select picks whichever is ready first select { case msg1 := <-ch1: fmt.Println("ch1:", msg1) case msg2 := <-ch2: fmt.Println("ch2:", msg2) case ch1 <- "ping": // send case fmt.Println("sent") default: // non-blocking fmt.Println("no msg") }
Timeout with time.After
import "time" ch := make(chan int) select { case v := <-ch: fmt.Println("got", v) case <-time.After( 2 * time.Second): fmt.Println("timed out!") } // Tick β repeat every interval ticker := time.NewTicker( 500 * time.Millisecond) defer ticker.Stop() <-ticker.C // receive tick
Fan-out / Fan-in Pattern
// Fan-out: one channel β many workers jobs := make(chan int, 100) for w := 0; w < 3; w++ { go worker(jobs) } // Fan-in: merge multiple channels func merge(cs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup for _, c := range cs { wg.Add(1) go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } }(c) } go func() { wg.Wait(); close(out) }() return out }
context Package
WithCancel β manual cancel
import "context" ctx, cancel := context.WithCancel( context.Background()) defer cancel() // always defer! go func(ctx context.Context) { for { select { case <-ctx.Done(): return // cancelled default: // keep working } } }(ctx) cancel() // stop the goroutine
WithTimeout & WithDeadline
// Auto-cancel after duration ctx, cancel := context.WithTimeout( context.Background(), 5*time.Second) defer cancel() // Use in HTTP / DB calls req, _ := http.NewRequestWithContext( ctx, "GET", url, nil) // Check reason for cancellation err := ctx.Err() // context.Canceled or // context.DeadlineExceeded
Pass context as the first argument to every function that does I/O or long work:
func DoWork(ctx context.Context, ...). Never store context in a struct.Worker Pool Pattern
Classic Worker Pool
func main() { jobs := make(chan int, 100) results := make(chan int, 100) // Spawn 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Send 9 jobs for j := 1; j <= 9; j++ { jobs <- j } close(jobs) // Collect all results for r := 1; r <= 9; r++ { <-results } } func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { results <- j * 2 } }
sync.Once Β· sync.Map Β· atomic
sync.Once β run exactly once
var ( instance *DB once sync.Once ) // Singleton pattern func GetDB() *DB { once.Do(func() { instance = newDB() }) return instance } // Do() runs once even if called // from 1000 goroutines at once
sync.Map β concurrent map
var m sync.Map // Store m.Store("key", "value") // Load val, ok := m.Load("key") // Delete m.Delete("key") // Iterate m.Range(func(k, v any) bool { fmt.Println(k, v) return true // continue })
sync/atomic β lock-free ops
import "sync/atomic" var counter int64 // Atomic add β no mutex needed atomic.AddInt64(&counter, 1) // Atomic load val := atomic.LoadInt64(&counter) // Atomic store atomic.StoreInt64(&counter, 0) // Compare and swap swapped := atomic.CompareAndSwapInt64( &counter, 0, 1)
Choosing the right tool: Use atomic for simple counters/flags (fastest). Use Mutex for protecting complex data structures. Use sync.Map when many goroutines read and few write. Use channels to pass ownership of data between goroutines.
Race Detector & Common Pitfalls
Race Detector β built into Go
# Run with race detector enabled go run -race main.go go test -race ./... go build -race -o app # Example race condition var x int go func() { x++ }() // DATA RACE! fmt.Println(x) # Output: # WARNING: DATA RACE # Write at 0x... by goroutine 6
Deadlock β all blocked
// Classic deadlock β goroutine // waits on itself ch := make(chan int) ch <- 1 // DEADLOCK β no receiver! // Mutex deadlock mu.Lock() mu.Lock() // DEADLOCK β double lock! // Fix: always pair Lock / Unlock // use defer mu.Unlock()
Goroutine Leak β never exits
// Leak: goroutine blocks forever func leak() { ch := make(chan int) go func() { val := <-ch // blocks forever process(val) }() // ch never sent to β leak! } // Fix: use context for cancellation select { case v := <-ch: process(v) case <-ctx.Done(): return }
Concurrency Primitives β Quick Reference
| Primitive | Package | Use when⦠| Key methods |
|---|---|---|---|
| goroutine | built-in | Run code concurrently | go func() |
| chan | built-in | Pass data between goroutines safely | make(chan T) <- close() |
| WaitGroup | sync | Wait for N goroutines to finish | Add Β· Done Β· Wait |
| Mutex | sync | Protect shared data (exclusive) | Lock Β· Unlock |
| RWMutex | sync | Many readers, rare writers | RLock Β· RUnlock Β· Lock Β· Unlock |
| Once | sync | Initialise exactly once (singleton) | Do(func()) |
| sync.Map | sync | Concurrent map (read-heavy) | Store Β· Load Β· Delete Β· Range |
| atomic | sync/atomic | Lock-free int/bool operations | AddInt64 Β· LoadInt64 Β· StoreInt64 Β· CAS |
| Context | context | Propagate cancellation & deadlines | WithCancel Β· WithTimeout Β· Done Β· Err |
| select | built-in | Multiplex over multiple channels | case Β· default |
| time.After | time | Add timeout to select | time.After(d) |
| time.Ticker | time | Repeat on interval | NewTicker Β· C Β· Stop |
Go Concurrency Mastery Checklist
| Goroutines & WaitGroup | Key point |
|---|---|
| Launch goroutines safely | go func(arg)(arg) |
| Wait for all goroutines | wg.Add Β· Done Β· Wait |
| Control parallelism | GOMAXPROCS(runtime.NumCPU()) |
| Avoid loop variable closure bug | pass i as argument |
| Sync & Channels | Key point |
|---|---|
| Protect shared data | Mutex + defer Unlock |
| Concurrent read-heavy map | RWMutex or sync.Map |
| Multiplex channels | select + default |
| Add timeout to any operation | time.After in select |
| Patterns & Safety | Key point |
|---|---|
| Cancel long-running goroutines | context.WithCancel |
| Build a worker pool | jobs + results channels |
| Detect races before production | go test -race |
| Prevent goroutine leaks | ctx.Done() in select |
Go series complete! β Β·
You've covered Go Basics and Concurrency. Explore more on Dataplexa β Java Collections, Rust, C++ STL, and more cheat sheets in the series.