Skip to content

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


import "sync"

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++
}

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

Section titled “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

Section titled “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
}

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.


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.


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()
}()

Pattern: Protecting a Struct with an Embedded Mutex

Section titled “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
}
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

Section titled “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

Section titled “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)

Section titled “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)
}

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

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