iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

sync.Mutex / WaitGroup

The sync package gives you the standard concurrency primitives: Mutex, RWMutex, WaitGroup, Once, Pool, Map, Cond. Use them when channels feel like the wrong shape; the rule of thumb is “share memory by communicating” for orchestration, locks for state.

Mutex, WaitGroup, Once, Pool, atomic

EXAMPLE
package main

import (
    "sync"
    "sync/atomic"
    "fmt"
    "time"
)

// 1) Mutex — protect shared state
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc()      { c.mu.Lock(); defer c.mu.Unlock(); c.n++ }
func (c *Counter) Value() int { c.mu.Lock(); defer c.mu.Unlock(); return c.n }

// Always pair Lock() with defer Unlock(). Panic-safe.
// Avoid copying a Mutex; use *Counter or embed by pointer.

// 2) RWMutex — many readers OR one writer
type Cache struct {
    mu sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(k string) (string, bool) {
    c.mu.RLock(); defer c.mu.RUnlock()
    v, ok := c.data[k]
    return v, ok
}

func (c *Cache) Set(k, v string) {
    c.mu.Lock(); defer c.mu.Unlock()
    c.data[k] = v
}

// RWMutex shines when reads dominate. Don't use for write-heavy work — overhead vs Mutex.

// 3) WaitGroup — wait for many goroutines to finish
func fetchAll(urls []string) {
    var wg sync.WaitGroup
    for _, u := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            // fetch url
        }(u)
    }
    wg.Wait()
}

// Add(n) BEFORE spawning, not inside the goroutine — race-free start.
// Done() guaranteed via defer.

// 4) Once — run code exactly once (thread-safe init)
var (
    cfg     *Config
    cfgOnce sync.Once
)

func getConfig() *Config {
    cfgOnce.Do(func() {
        cfg = loadConfig()
    })
    return cfg
}

// Even with N concurrent callers, loadConfig runs once and they all block until it's done.

// 5) Pool — reuse expensive objects
var bufPool = sync.Pool{
    New: func() any { return make([]byte, 4096) },
}

func process(r io.Reader) {
    buf := bufPool.Get().([]byte)
    defer bufPool.Put(buf)
    // use buf without allocating
    _, _ = io.ReadFull(r, buf)
}

// Great for GC pressure (buffers, JSON encoders, etc.).
// Pool may evict items between uses; don't rely on stored state.

// 6) Map — concurrent map for write-once / read-many
var cache sync.Map
cache.Store("key", "value")
v, ok := cache.Load("key")
cache.Delete("key")
cache.Range(func(k, v any) bool { fmt.Println(k, v); return true })

// sync.Map is OPTIMIZED for: stable keys with infrequent writes, or many goroutines reading/writing
// disjoint keys. For the typical write-heavy concurrent map, a plain map + RWMutex is faster.

// 7) Cond — wait for a state change (rare; channels usually cleaner)
var (
    mu sync.Mutex
    cond = sync.NewCond(&mu)
    ready bool
)

func consumer() {
    mu.Lock()
    for !ready {
        cond.Wait()                       // releases mu while sleeping
    }
    mu.Unlock()
}

func producer() {
    mu.Lock()
    ready = true
    cond.Broadcast()                       // or Signal() for one
    mu.Unlock()
}

// 8) sync/atomic — lock-free counters + flags
var hits atomic.Int64                       // Go 1.19+
hits.Add(1)
hits.Load()
hits.Store(0)
hits.CompareAndSwap(0, 1)

// Use atomics for primitives where a Mutex would be overkill.
// Pre-1.19: atomic.AddInt64(&hits, 1), atomic.LoadInt64(&hits)

// atomic.Value — for any type, with type-stable Store/Load
var config atomic.Value
config.Store(&Config{Host: "localhost"})
cfg := config.Load().(*Config)

// 9) errgroup — wait + first-error cancellation (golang.org/x/sync/errgroup)
import "golang.org/x/sync/errgroup"

func fetchAllSafe(ctx context.Context, urls []string) error {
    g, gctx := errgroup.WithContext(ctx)
    g.SetLimit(8)
    for _, u := range urls {
        u := u
        g.Go(func() error {
            return fetch(gctx, u)
        })
    }
    return g.Wait()                          // first error cancels gctx + returned here
}

// errgroup is what most production code uses instead of bare WaitGroup.

// 10) Semaphore — bounded concurrency
import "golang.org/x/sync/semaphore"

sem := semaphore.NewWeighted(8)
for _, item := range items {
    item := item
    sem.Acquire(ctx, 1)
    go func() {
        defer sem.Release(1)
        process(item)
    }()
}

// 11) Channels vs sync primitives — when to use which
// Channels:
//   • Communication between goroutines (pipeline, fan-out / fan-in)
//   • Cancellation (close + select)
//   • Synchronous handoff (send/receive)
//
// sync primitives:
//   • Protecting shared mutable state
//   • Wait for completion (WaitGroup / errgroup)
//   • Lazy initialisation (Once)
//   • Buffer reuse (Pool)
//   • Lock-free counters (atomic)

// 12) Debugging concurrent code
// go run -race main.go                       // race detector — enable in tests + dev
// go test -race ./...                         // CI standard
// runtime/pprof + 'go tool pprof'             // profiling

// 13) Race detector example finding a real bug
func main() {
    var count int
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++                              // RACE — no synchronisation
        }()
    }
    wg.Wait()
    fmt.Println(count)                          // < 1000 due to lost increments
}
// go run -race main.go → reports the race.

// Fix:
//   atomic.Int64 or Mutex around count.

// 14) Common bugs
// • Copying a Mutex — gives DIFFERENT locks; embed via pointer
// • Releasing a Mutex you don't hold — panic
// • Forgetting wg.Add BEFORE go — wg.Wait may return before all workers register
// • Recursive locking — Mutex isn't reentrant; deadlock
// • Holding a Mutex across an unrelated channel send/receive — deadlock under contention
// • RWMutex starvation — long writer queue blocks new readers; use RWMutex only when needed
// • Pool storing stateful objects — reset state on Get
// • sync.Map for write-heavy maps — slower than RWMutex + map
// • Atomic on int64 fields not 8-byte aligned (32-bit ARM) — use atomic.Int64 wrapper (Go 1.19+)
// • Forgetting to call Done in branching code — defer it
// • Cond.Wait without a for loop checking condition — spurious wakeups

Why it matters

Use channels for orchestration, the sync package for state: Mutex/RWMutex for shared mutable data, WaitGroup (or better, errgroup) for completion, Once for lazy init, Pool for buffer reuse, atomic for lock-free counters. Always run tests with -race to catch the bugs concurrent code is famous for.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(i int) { defer wg.Done(); work(i) }(i)
}
wg.Wait()
Try it Yourself »

Discussion

Loading…