sync/atomic
Why atomics?
Mutexes protect arbitrary code sections but carry overhead: a sync.Mutex lock/unlock pair costs ~20โ50 ns even uncontended. For a single shared integer โ a hit counter, a flag, an index โ atomic operations provide lock-free synchronisation at the hardware level.
The typed atomic API (Go 1.19+)
Go 1.19 introduced value types in sync/atomic that are safe and ergonomic:
atomic.Int64, atomic.Int32, atomic.Uint64, atomic.Bool, atomic.Pointer[T] โ all follow the same pattern.
atomic.Bool โ single flag
No mutex needed for a single boolean flag.
Compare-and-swap (CAS)
CAS atomically does: if current == old, set to new and return true; else return false.
CAS is the building block for lock-free data structures and single-flight deduplication.
atomic.Pointer[T]
Atomically swap a pointer to any value โ useful for hot config reloading:
When to use atomics vs mutexes
| Use atomics when | Use mutexes when |
|---|---|
| Single integer / bool / pointer | Protecting a struct with multiple fields |
| Extremely high-frequency updates | Complex conditional logic across fields |
| Building lock-free algorithms | Correctness is more important than peak perf |
Default to mutexes. Reach for atomics only when profiling shows mutex contention is a bottleneck.
The old function-based API
Before Go 1.19, the API used standalone functions. You'll see this in older code:
The newer value types (atomic.Int64) are safer โ they cannot be accidentally copied.
Knowledge Check
When should you prefer sync/atomic over sync.Mutex?
What does atomic.Int64.CompareAndSwap(old, new) do?
Why are the typed atomics (atomic.Int64) safer than the function-based API (atomic.AddInt64(&n, 1))?