Neural Architecture Hub
Go

Go sync

Go sync package primitives, locks, once initialization, wait groups, pools, maps, and condition variables reference.

Go sync Package Developer Guide


Import

import "sync"

sync.Mutex — Mutual Exclusion Lock

var mu sync.Mutex

mu.Lock()           // Acquire the lock (blocks if already held)
mu.Unlock()         // Release the lock
mu.TryLock()        // Try to acquire lock; returns false if already held (Go 1.18+)

Basic usage pattern

var (
    mu      sync.Mutex
    counter int
)

func increment() {
    mu.Lock()
    defer mu.Unlock()   // Always defer Unlock to avoid deadlocks
    counter++
}

sync.RWMutex — Reader/Writer Mutex

var rw sync.RWMutex

rw.Lock()           // Acquire write lock (exclusive)
rw.Unlock()         // Release write lock
rw.RLock()          // Acquire read lock (shared; multiple readers allowed)
rw.RUnlock()        // Release read lock
rw.TryLock()        // Non-blocking write lock attempt (Go 1.18+)
rw.TryRLock()       // Non-blocking read lock attempt  (Go 1.18+)

Usage pattern

var (
    rw    sync.RWMutex
    cache = map[string]string{}
)

func read(key string) string {
    rw.RLock()
    defer rw.RUnlock()
    return cache[key]
}

func write(key, val string) {
    rw.Lock()
    defer rw.Unlock()
    cache[key] = val
}

sync.WaitGroup — Wait for Goroutines to Finish

var wg sync.WaitGroup

wg.Add(n)       // Increment counter by n (call before launching goroutine)
wg.Done()       // Decrement counter by 1 (call inside goroutine when finished)
wg.Wait()       // Block until counter reaches 0

Usage pattern

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Println("worker", id)
    }(i)
}

wg.Wait()   // Block until all 5 goroutines call Done()
fmt.Println("all done")

sync.Once — Execute a Function Exactly Once

var once sync.Once

once.Do(fn)     // Calls fn only on the first invocation; subsequent calls are no-ops

Usage pattern — lazy singleton initialization

var (
    once     sync.Once
    instance *DB
)

func GetDB() *DB {
    once.Do(func() {
        instance = &DB{} // Expensive init runs only once
    })
    return instance
}

sync.Map — Concurrent-Safe Map

var m sync.Map

m.Store(key, value)                     // Set a key-value pair
val, ok := m.Load(key)                  // Get value; ok is false if key absent
val, loaded := m.LoadOrStore(key, val)  // Store if absent; return existing if present
val, loaded := m.LoadAndDelete(key)     // Load and then delete the key atomically
m.Delete(key)                           // Delete a key
m.Range(func(k, v any) bool { ... })    // Iterate all entries; return false to stop
m.Swap(key, value)                      // Store and return previous value (Go 1.20+)
m.CompareAndSwap(key, old, new)         // Swap only if current value == old (Go 1.20+)
m.CompareAndDelete(key, old)            // Delete only if current value == old (Go 1.20+)

Usage pattern

var m sync.Map

m.Store("hits", 0)

// Concurrent reads and writes are safe without extra locking
go func() { m.Store("hits", 42) }()
go func() {
    if v, ok := m.Load("hits"); ok {
        fmt.Println(v)
    }
}()

// Iterate all keys
m.Range(func(k, v any) bool {
    fmt.Printf("%v => %v\n", k, v)
    return true // continue iteration
})

When to use: Mostly-read maps with infrequent writes, or many goroutines writing disjoint keys. For heavy mixed workloads prefer map + sync.RWMutex.


sync.Pool — Reusable Object Pool

pool := &sync.Pool{
    New: func() any { return new(bytes.Buffer) }, // Called when pool is empty
}

obj := pool.Get()           // Retrieve object from pool (or call New)
pool.Put(obj)               // Return object to pool for reuse

Usage pattern — reducing allocations

var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

func process(data string) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()         // Always reset before returning!
        bufPool.Put(buf)
    }()
    buf.WriteString(data)
    return buf.String()
}

Note: Objects in the pool may be evicted by the GC at any time. Never store objects with finalization logic or file handles in a Pool.


sync.Cond — Condition Variable

cond := sync.NewCond(&mu)   // Create Cond bound to a Locker (Mutex or RWMutex)

cond.Wait()                 // Atomically unlock mu and suspend; re-locks on wake
cond.Signal()               // Wake one goroutine waiting on cond
cond.Broadcast()            // Wake all goroutines waiting on cond

Usage pattern — producer/consumer

var (
    mu    sync.Mutex
    cond  = sync.NewCond(&mu)
    ready bool
)

// Consumer goroutine
go func() {
    mu.Lock()
    for !ready {            // Always loop — spurious wakeups can occur
        cond.Wait()
    }
    fmt.Println("got signal!")
    mu.Unlock()
}()

// Producer goroutine
go func() {
    mu.Lock()
    ready = true
    cond.Broadcast()        // Wake all waiting goroutines
    mu.Unlock()
}()

Common Patterns & Advanced Usage

Pattern: Protecting a Struct with an Embedded Mutex

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
}

Pattern: Read-Heavy Cache with RWMutex

type Cache struct {
    mu    sync.RWMutex
    items map[string]any
}

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

func (c *Cache) Set(key string, val any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = val
}

Pattern: Worker Pool with WaitGroup + Channel

jobs := make(chan int, 100)
var wg sync.WaitGroup

// Launch workers
for w := 0; w < 4; w++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        for j := range jobs {
            process(j)
        }
    }()
}

// Send jobs
for j := 0; j < 20; j++ {
    jobs <- j
}
close(jobs)

wg.Wait()

Pattern: Double-Checked Locking with sync.Once

// Avoid: manually layering Once + Mutex — sync.Once already handles this safely.
// Prefer:
var (
    once   sync.Once
    client *http.Client
)

func getClient() *http.Client {
    once.Do(func() {
        client = &http.Client{Timeout: 10 * time.Second}
    })
    return client
}

Pattern: Timed Lock Attempt (simulated TryLock loop)

// TryLock with deadline (Go 1.18+)
deadline := time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
    if mu.TryLock() {
        defer mu.Unlock()
        // critical section
        break
    }
    time.Sleep(5 * time.Millisecond)
}

Pitfalls & Best Practices

DO                                          DON'T
─────────────────────────────────────────   ──────────────────────────────────────────
Always defer Unlock after Lock              Call Unlock in a separate if-branch (easy to miss)
Check the condition in a for loop in Wait   Use if instead of for before cond.Wait()
Reset Pool objects before Put               Return dirty objects to the pool
Use RWMutex for read-heavy workloads        Always reach for Mutex (readers block readers)
Pass *sync.Mutex by pointer                 Copy a Mutex/WaitGroup after first use
Call wg.Add before launching goroutine      Call wg.Add inside the goroutine (race condition)
Use sync.Once for one-time initialization   Re-initialize manually with flags + Mutex

Quick Reference Card

TypeUse CaseKey Methods
MutexExclusive access to shared resourceLock, Unlock, TryLock
RWMutexMany readers, few writersRLock, RUnlock, Lock, Unlock
WaitGroupWait for N goroutines to completeAdd, Done, Wait
OnceExactly-once initializationDo
MapConcurrent-safe map without external lockingStore, Load, Delete, Range
PoolReuse temporary objects to reduce GC pressureGet, Put
CondGoroutines waiting on a condition to become trueWait, Signal, Broadcast

On this page