sync/atomic
Counters and whole-value swaps that are safe across goroutines without taking a lock.
var n atomic.Int64; n.Add(1); n.Load()An atomic operation happens in one indivisible step. No other goroutine can observe it half done. The sync/atomic package provides those operations on single values. When the shared state is one number or one pointer, atomics are usually cheaper than a sync.Mutex, because nothing has to wait.
The typed values
Go 1.19 added types such as atomic.Int64, atomic.Bool and atomic.Pointer[T]. Their zero values are ready to use. Load reads, Store writes, Add adds and returns the new value, Swap replaces and returns the old one, and CompareAndSwap(old, new) writes only if the current value is still old, reporting whether it did.
The older function form takes a pointer to a plain variable: atomic.AddInt64(&n, 1) on a var n int64. It is the same machine operation as n.Add(1) on an atomic.Int64. Prefer the typed form in new code, because a plain int64 can still be read with ordinary n by accident, and that read is a race.
atomic.Value
atomic.Value holds one value of any single type and swaps it whole. It is the tool for publishing a freshly loaded configuration struct to every goroutine at once. Since Go 1.19, atomic.Pointer[T] does the same job with a compile-time type and no type assertion.
What trips people up
Atomics protect one value, not an invariant across two. If two fields must agree, use a mutex.
Example
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// atomic.Int64 (Go 1.19) is a counter any goroutine may update.
// Its zero value is zero, so no setup is needed.
var hits atomic.Int64
// atomic.Value holds one value of a single concrete type.
var region atomic.Value
region.Store("eu-west-1")
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
// Read, add and write happen as one indivisible step.
hits.Add(1)
// Load returns any, so assert the type back to string.
_ = region.Load().(string)
})
}
wg.Wait()
fmt.Println("hits:", hits.Load())
fmt.Println("region:", region.Load().(string))
}
// Output:
// hits: 50
// region: eu-west-1When to use it
- Counting requests, errors or cache hits in a handler that runs on thousands of goroutines.
- A shutdown flag that every worker checks on each pass through its loop.
- Swapping in reloaded configuration or a rebuilt lookup table without blocking readers.
More examples
The function form and the typed form side by side
Both halves compile to the same instructions. The difference is what the compiler lets you do by mistake: <code>plain++</code> and <code>fmt.Println(plain)</code> are legal and are races, while <code>atomic.Int64</code> has no way to be read non-atomically. One more reason for the typed form: with the function form on ARM, 386 and 32-bit MIPS, the caller must arrange for the 64-bit value to be 8-byte aligned.
// Longhand: a plain int64 plus package-level functions.
var plain int64
func bumpPlain() int64 {
return atomic.AddInt64(&plain, 1)
}
func readPlain() int64 {
return atomic.LoadInt64(&plain)
}
// Shorthand: the same operations as methods on atomic.Int64.
var typed atomic.Int64
func bumpTyped() int64 {
return typed.Add(1)
}
func readTyped() int64 {
return typed.Load()
}Publishing reloaded configuration with atomic.Pointer
Reach for this when a background goroutine reloads settings while request handlers keep reading them. Build a brand new <code>Config</code>, then store the pointer. Never modify a Config that has already been stored, because readers hold the same pointer and would see the fields change under them. <code>atomic.Pointer[Config]</code> is the typed alternative to <code>atomic.Value</code>: no type assertion, and the wrong type will not compile.
type Config struct {
TimeoutMS int
Upstream string
}
type Holder struct {
cur atomic.Pointer[Config] // Go 1.19 and later
}
// Get returns the configuration in force right now.
func (h *Holder) Get() *Config { return h.cur.Load() }
// Set swaps in a whole new configuration in one step.
// Readers see either the old one or the new one, never a mix.
func (h *Holder) Set(c *Config) { h.cur.Store(c) }CompareAndSwap to record a maximum
<code>Add</code> covers plain counting, but "keep the largest value seen" is a read followed by a conditional write, and those two steps can be interleaved. <code>CompareAndSwap</code> writes only if the value is unchanged since the Load, so the loop eventually succeeds. This retry loop is the standard lock-free pattern; if you find yourself needing it across two variables, switch to a mutex.
// recordMax raises slowest to d if d is larger. It retries because
// another goroutine may raise it between the Load and the swap.
func recordMax(slowest *atomic.Int64, d int64) {
for {
cur := slowest.Load()
if d <= cur {
return
}
if slowest.CompareAndSwap(cur, d) {
return
}
// The value moved. Loop and compare against the new one.
}
}
Discussion