Go Concurrency Cheat Sheet β€” Goroutines, WaitGroup, Mutex, Select, Context | Dataplexa

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

go keyword
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 package
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

chan keyword
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

multi-channel
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

cancellation Β· timeout
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

production 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

advanced sync
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

debug guide
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

cheat reference
PrimitivePackageUse 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

sheet 2 complete
Goroutines & WaitGroupKey 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 & ChannelsKey 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 & SafetyKey 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.