Sync Primitives
When channels are not enough
Section titled “When channels are not enough”Channels excel at transferring ownership of data between goroutines. But sometimes you simply need to protect a shared value that many goroutines read and write — a cache, a counter, a connection pool. For these cases the sync package provides lower-level primitives.
sync.Mutex
Section titled “sync.Mutex”A sync.Mutex is a mutual exclusion lock. Only one goroutine can hold the lock at a time; all others block on Lock() until the holder calls Unlock().
var mu sync.Mutexvar count int
mu.Lock()count++mu.Unlock()Always use defer mu.Unlock() immediately after mu.Lock() so the lock is released even if the function panics or returns early.
sync.RWMutex
Section titled “sync.RWMutex”sync.RWMutex is an optimized variant for the read-heavy case. Multiple goroutines can hold a read lock simultaneously with RLock/RUnlock. A write lock (Lock/Unlock) is exclusive — it waits for all readers to finish, then blocks new readers until the writer is done.
Use it when reads are far more frequent than writes (e.g., an in-memory cache).
sync.Once
Section titled “sync.Once”sync.Once executes a function exactly once, regardless of how many goroutines call Do concurrently. The canonical use case is lazy, thread-safe initialization:
var once sync.Oncevar instance *Config
func getConfig() *Config { once.Do(func() { instance = loadConfig() }) return instance}sync/atomic
Section titled “sync/atomic”The sync/atomic package provides lock-free operations on integer and pointer types using CPU hardware instructions. For simple counters or flags, atomics are faster than a mutex because there is no lock contention or context switching.
Key operations: atomic.AddInt64, atomic.LoadInt64, atomic.StoreInt64, atomic.CompareAndSwapInt64.
Channels vs mutexes — a practical guide
Section titled “Channels vs mutexes — a practical guide”| Use channels when… | Use mutexes when… |
|---|---|
| Transferring ownership of data | Protecting a shared cache or struct |
| Coordinating goroutine lifecycles | Managing a simple counter or flag |
| Pipelines and fan-out patterns | Read-heavy data with sync.RWMutex |
package main
import ( "fmt" "sync" "sync/atomic")
type SafeCounter struct { mu sync.Mutex v int}
func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.v++}
func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.v}
func main() { // sync.Mutex: 100 goroutines each increment once -> 100 var wg sync.WaitGroup c := &SafeCounter{} for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() fmt.Println("counter:", c.Value()) // 100
// sync.Once: function runs exactly once despite 3 calls var once sync.Once for i := 0; i < 3; i++ { once.Do(func() { fmt.Println("only once") }) }
// sync/atomic: 50 goroutines each add 1 -> 50 var atomicCount int64 for i := 0; i < 50; i++ { wg.Add(1) go func() { defer wg.Done() atomic.AddInt64(&atomicCount, 1) }() } wg.Wait() fmt.Println("atomic count:", atomic.LoadInt64(&atomicCount)) // 50}Loading Go runtime (first run only, ~8 MB)…