Sync Atomic #
The sync/atomic package provides low-level operations guaranteed to run atomically — meaning the operation can’t be interrupted midway by another goroutine. This differs from Mutex, which locks an entire critical section: atomic operations work at the CPU instruction level, making them much faster. For simple cases like counters, boolean flags, or frequently-read caches — where a Mutex feels like overkill — atomic is the right choice. Go 1.19 introduced generic types like atomic.Int64, atomic.Bool, and atomic.Pointer[T] that are far more ergonomic than the previous low-level functions. Understanding when to use atomic vs Mutex is the key to writing correct and efficient concurrent code.
An Overview of the sync/atomic Package #
flowchart TD
A["package sync/atomic"] --> OldAPI["Old API (functions)\nGo 1.x"]
A --> NewAPI["New API (types)\nGo 1.19+"]
OldAPI --> OF1["atomic.AddInt32/64\natomic.LoadInt32/64\natomic.StoreInt32/64\natomic.SwapInt32/64\natomic.CompareAndSwapInt32/64"]
OldAPI --> OF2["atomic.LoadPointer\natomic.StorePointer\natomic.LoadUintptr"]
OldAPI --> OV["atomic.Value\nStore / Load / Swap\nCompareAndSwap"]
NewAPI --> NT1["atomic.Int32 / Int64\natomic.Uint32 / Uint64\natomic.Uintptr"]
NewAPI --> NT2["atomic.Bool\nStore / Load / Swap\nCompareAndSwap"]
NewAPI --> NT3["atomic.Pointer[T]\nStore / Load / Swap\nCompareAndSwap"]
subgraph When["Use atomic if:"]
K1["Simple counters\n(hit counts, request counts)"]
K2["Boolean flags\n(shutdown, initialized)"]
K3["Atomic pointer swaps\n(hot reload config)"]
K4["Very frequent operations\nwhere Mutex is too expensive"]
end
style A fill:#4f86c6,color:#fff
style OldAPI fill:#fff3e0
style NewAPI fill:#e8f5e9
style When fill:#e3f2fdThe New API — Atomic Types (Go 1.19+) #
Go 1.19 introduced more ergonomic atomic types. Use these for new projects:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// atomic.Int64 — a counter safe for concurrent access
var counter atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Add(1)
}()
}
wg.Wait()
fmt.Println("Counter:", counter.Load()) // always 1000
// atomic.Bool — a boolean flag
var shutdown atomic.Bool
shutdown.Store(false)
go func() {
// Simulate a shutdown signal
shutdown.Store(true)
}()
// Swap — set a new value, return the old value
previous := shutdown.Swap(true)
fmt.Println("Previous:", previous)
// CompareAndSwap — only change if the current value matches
succeeded := shutdown.CompareAndSwap(true, false)
fmt.Println("CAS succeeded:", succeeded)
fmt.Println("Now:", shutdown.Load())
// atomic.Pointer[T] — a safe pointer
type Config struct {
Host string
Port int
}
var configPtr atomic.Pointer[Config]
configPtr.Store(&Config{"localhost", 8080})
cfg := configPtr.Load()
fmt.Printf("Config: %s:%d\n", cfg.Host, cfg.Port)
}
All the New Atomic Types #
// Integers
var i32 atomic.Int32
var i64 atomic.Int64
var u32 atomic.Uint32
var u64 atomic.Uint64
var uptr atomic.Uintptr
// Boolean
var b atomic.Bool
// Generic pointers
var p atomic.Pointer[MyStruct]
// The same methods for all:
// .Load() T — read the current value
// .Store(val T) — store a new value
// .Swap(val T) T — store val, return the old value
// .CompareAndSwap(old, new T) bool — change only if == old
// .Add(delta T) T — add delta, return the new value (numeric only)
The Old API — Atomic Functions #
Before Go 1.19, atomic operations were done via functions. Still valid and common in existing code:
import "sync/atomic"
// AddInt64 — add a delta atomically, return the new value
var counter int64
atomic.AddInt64(&counter, 1) // counter++
atomic.AddInt64(&counter, -1) // counter--
atomic.AddInt64(&counter, 10) // counter += 10
// LoadInt64 — read the value atomically
val := atomic.LoadInt64(&counter)
// StoreInt64 — store a value atomically
atomic.StoreInt64(&counter, 0) // reset
// SwapInt64 — store and return the old value
previous := atomic.SwapInt64(&counter, 100)
// CompareAndSwapInt64 (CAS) — change only if equal to old
succeeded := atomic.CompareAndSwapInt64(&counter, 100, 200)
// counter changes to 200 only if it's currently == 100
// 32-bit versions
var c32 int32
atomic.AddInt32(&c32, 1)
atomic.LoadInt32(&c32)
atomic.StoreInt32(&c32, 0)
atomic.CompareAndSwapInt32(&c32, 0, 1)
// Unsigned
var uc uint64
atomic.AddUint64(&uc, 1)
atomic.LoadUint64(&uc)
atomic.Value — Storing Any Value #
atomic.Value allows atomically storing and reading values of type interface{} — useful for hot-reloading configuration or frequently-read data:
sequenceDiagram
participant Writer as Writer Goroutine
participant AV as atomic.Value
participant R1 as Reader 1
participant R2 as Reader 2
participant R3 as Reader 3
R1->>AV: Load() → config v1
R2->>AV: Load() → config v1
Writer->>AV: Store(config v2)
R3->>AV: Load() → config v2
R1->>AV: Load() → config v2
Note over AV: No locks, no blocking\nAll operations atomicimport "sync/atomic"
// atomic.Value for hot-reloading configuration
type Config struct {
Host string
Port int
Debug bool
MaxConn int
}
var globalConfig atomic.Value
func init() {
// Store the initial config
globalConfig.Store(&Config{
Host: "localhost",
Port: 8080,
Debug: false,
MaxConn: 100,
})
}
// Read the config — very fast, no locks
func getConfig() *Config {
return globalConfig.Load().(*Config)
}
// Update the config — usually from a background goroutine
func updateConfig(cfg *Config) {
globalConfig.Store(cfg)
}
// Hot-reload from a file
func watchConfig(path string, ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cfg, err := loadConfigFromFile(path)
if err != nil {
log.Printf("failed to reload config: %v", err)
continue
}
updateConfig(cfg)
log.Println("config reloaded successfully")
}
}
}
// Usage in a handler — very efficient
func apiHandler(w http.ResponseWriter, r *http.Request) {
cfg := getConfig() // read the config without a lock
if cfg.Debug {
log.Printf("request: %s %s", r.Method, r.URL.Path)
}
// ...
}
atomic.Value Rules #
var v atomic.Value
// RULE 1: the stored type must be consistent
v.Store(42) // store an int
v.Store("string") // PANIC! the type changed from int to string
// CORRECT: always store the same type
v.Store(42)
v.Store(100) // OK — both ints
// RULE 2: can't Store a nil interface
var cfg *Config = nil
v.Store(cfg) // PANIC! can't Store nil
// CORRECT: store a pointer to a zero value
v.Store(&Config{}) // OK — a valid pointer
// RULE 3: Load before the first Store returns nil
var v2 atomic.Value
result := v2.Load() // nil — never stored
if result != nil {
cfg := result.(*Config)
_ = cfg
}
// Swap — store and return the old value (atomic)
old := v.Swap(200)
fmt.Println(old) // 100 (the previous value)
// CompareAndSwap — change only if equal to old
succeeded := v.CompareAndSwap(200, 300)
fmt.Println(succeeded) // true
Comparison: atomic vs Mutex #
flowchart TD
Q{"What needs\nto be synchronized?"} --> Simple["A single value\n(int, bool, pointer)"]
Q --> Complex["Several values\nthat must be consistent\nwith each other"]
Q --> ReadHeavy["Very frequent reads\nrare writes"]
Q --> WriteHeavy["Writes as frequent\nas reads"]
Simple --> Atomic["sync/atomic\nFaster\nSimpler for single values"]
Complex --> Mutex["sync.Mutex\nMust be one atomic unit"]
ReadHeavy --> RWMutex["sync.RWMutex\nor atomic.Value"]
WriteHeavy --> Mutex2["sync.Mutex\nor atomic if it's a single value"]
style Atomic fill:#e8f5e9
style Mutex fill:#e3f2fd
style RWMutex fill:#e3f2fd
style Mutex2 fill:#e3f2fd// Performance comparison (benchmark illustration):
// atomic.AddInt64: ~5 ns/op
// Mutex.Lock + value++ + Mutex.Unlock: ~25 ns/op
// (~5x faster for single operations)
// ANTI-PATTERN: atomic for operations that must be consistent together
var total int64
var count int64
// This is NOT safe! total and count can be inconsistent
// (another goroutine could read between the two Adds)
atomic.AddInt64(&total, price)
atomic.AddInt64(&count, 1)
// average := total/count could be inconsistent!
// CORRECT: Mutex for several values that must stay consistent together
var mu sync.Mutex
var total2 int64
var count2 int64
mu.Lock()
total2 += price
count2++
mu.Unlock()
// total2 and count2 are always consistent
// CORRECT: atomic for single independent values
var requestCount atomic.Int64
var errorCount atomic.Int64
// Both are independent — atomic is fine
requestCount.Add(1)
if err != nil {
errorCount.Add(1)
}
Compare-And-Swap (CAS) — The Key Operation #
CAS is a fundamental operation in lock-free concurrent programming. It changes a value only if the current value equals the expected one:
flowchart TD
CAS["CompareAndSwap(old, new)"] --> Check{"current value\n== old?"}
Check -- Yes --> Update["Set the value to new\nReturn true"]
Check -- No --> NoUpdate["No change\nReturn false"]
subgraph Example["Example: Lock-Free Increment"]
L1["Load the current value: n"]
L2["Compute n+1"]
L3["CAS(n, n+1)"]
L4{succeeded?}
L5["Done"]
L6["Repeat (loop)"]
L1 --> L2 --> L3 --> L4
L4 -- Yes --> L5
L4 -- No --> L6 --> L1
end
style Update fill:#e8f5e9
style NoUpdate fill:#fce4ec// The CAS loop pattern — for operations needing atomic read-modify-write
func incrementSafe(counter *int64) {
for {
old := atomic.LoadInt64(counter)
new := old + 1
if atomic.CompareAndSwapInt64(counter, old, new) {
return // succeeded
}
// failed — the value changed between Load and CAS — repeat
}
}
// With the new types — much cleaner
var counter atomic.Int64
counter.Add(1) // Add already does the CAS loop internally
// CAS for a state machine — only allow valid transitions
type State int32
const (
StateIdle State = 0
StateRunning State = 1
StateStopped State = 2
)
var state atomic.Int32
func start() bool {
// Can only start if currently Idle
return state.CompareAndSwap(int32(StateIdle), int32(StateRunning))
}
func stop() bool {
// Can only stop if currently Running
return state.CompareAndSwap(int32(StateRunning), int32(StateStopped))
}
// Usage
if start() {
fmt.Println("started successfully")
} else {
fmt.Println("can't start — not in the Idle state")
}
Production Usage Patterns #
Efficient Metric Counters #
// Metric counters for monitoring — frequently updated, frequently read
type Metrics struct {
RequestTotal atomic.Int64
RequestError atomic.Int64
BytesReceived atomic.Int64
BytesSent atomic.Int64
ActiveConns atomic.Int64
}
var metrics Metrics
func handlerWithMetrics(w http.ResponseWriter, r *http.Request) {
metrics.RequestTotal.Add(1)
metrics.ActiveConns.Add(1)
defer metrics.ActiveConns.Add(-1)
// Count the received bytes
body, err := io.ReadAll(r.Body)
if err != nil {
metrics.RequestError.Add(1)
http.Error(w, "error", 500)
return
}
metrics.BytesReceived.Add(int64(len(body)))
// Process and send the response
response := []byte(`{"status":"ok"}`)
w.Write(response)
metrics.BytesSent.Add(int64(len(response)))
}
// An endpoint to expose the metrics
func metricsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "request_total %d\n", metrics.RequestTotal.Load())
fmt.Fprintf(w, "request_error %d\n", metrics.RequestError.Load())
fmt.Fprintf(w, "bytes_received %d\n", metrics.BytesReceived.Load())
fmt.Fprintf(w, "bytes_sent %d\n", metrics.BytesSent.Load())
fmt.Fprintf(w, "active_connections %d\n", metrics.ActiveConns.Load())
}
Singleton with Once vs atomic.Pointer #
// Approach 1: sync.Once — one-time initialization
var (
dbOnce sync.Once
dbInstance *sql.DB
)
func GetDB() *sql.DB {
dbOnce.Do(func() {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
dbInstance = db
})
return dbInstance
}
// Approach 2: atomic.Pointer — updatable (for hot-reload)
var dbPtr atomic.Pointer[sql.DB]
func GetDBv2() *sql.DB {
return dbPtr.Load()
}
func SetDB(db *sql.DB) {
dbPtr.Store(db)
}
// Useful for testing — can swap the DB with a test DB
func TestHandler(t *testing.T) {
testDB := setupTestDB(t)
SetDB(testDB)
defer SetDB(productionDB()) // restore after the test
// ...
}
A Lock-Free Rate Limiter #
// A simple rate limiter using atomic
type RateLimiter struct {
limit int64
window time.Duration
count atomic.Int64
resetAt atomic.Int64 // Unix nano
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
limit: int64(limit),
window: window,
}
rl.resetAt.Store(time.Now().Add(window).UnixNano())
return rl
}
func (rl *RateLimiter) Allow() bool {
now := time.Now().UnixNano()
resetAt := rl.resetAt.Load()
// Reset the window if it has passed
if now >= resetAt {
newResetAt := time.Now().Add(rl.window).UnixNano()
if rl.resetAt.CompareAndSwap(resetAt, newResetAt) {
rl.count.Store(0) // reset the count
}
}
// Increment and check the limit
current := rl.count.Add(1)
return current <= rl.limit
}
// Usage
limiter := NewRateLimiter(100, time.Second) // 100 req/second
func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
// process the request
}
A Cache with Atomic Swaps #
// A simple cache with atomic — no lock needed when reading
type AtomicCache[K comparable, V any] struct {
mu sync.Mutex // only for writes
data atomic.Pointer[map[K]V]
}
func NewAtomicCache[K comparable, V any]() *AtomicCache[K, V] {
c := &AtomicCache[K, V]{}
m := make(map[K]V)
c.data.Store(&m)
return c
}
// Get — no lock needed, very fast
func (c *AtomicCache[K, V]) Get(key K) (V, bool) {
m := c.data.Load()
v, ok := (*m)[key]
return v, ok
}
// Set — needs a lock for writes, but creates a new copy so Get stays lock-free
func (c *AtomicCache[K, V]) Set(key K, val V) {
c.mu.Lock()
defer c.mu.Unlock()
// Make a copy of the existing map
old := c.data.Load()
newMap := make(map[K]V, len(*old)+1)
for k, v := range *old {
newMap[k] = v
}
newMap[key] = val
// Atomic swap — readers in flight get either the old or the new map,
// but both are consistent (no half-updated map)
c.data.Store(&newMap)
}
// Usage
cache := NewAtomicCache[string, *User]()
// Thousands of goroutines can Get simultaneously without locks
go func() {
user, ok := cache.Get("budi")
if ok {
fmt.Println(user.Name)
}
}()
// Occasional writes
cache.Set("budi", &User{Name: "Budi"})
When NOT to Use atomic #
flowchart TD
Check{"Need operations on\nmore than one variable\nconsistently?"}
Check -- Yes --> UseMutex["Use sync.Mutex\natomic isn't enough!"]
Check -- No --> SimpleOp{"Operation\nneeded?"}
SimpleOp -- "Load / Store\nAdd / Swap\nCAS on one value" --> UseAtomic["Use sync/atomic\nFaster"]
SimpleOp -- "More complex\nlogic" --> UseMutex2["Use sync.Mutex\nSafer"]
subgraph Danger["✗ Anti-Patterns — Don't Do These"]
B1["Two atomic ops that\nmust stay consistent together"]
B2["Read-modify-write\nof many variables at once"]
B3["Replacing Mutex with atomic\nwithout understanding memory ordering"]
end
style UseMutex fill:#e3f2fd
style UseMutex2 fill:#e3f2fd
style UseAtomic fill:#e8f5e9
style Danger fill:#fce4ec// ANTI-PATTERN: two atomic ops that should be one unit
var success atomic.Int64
var failure atomic.Int64
// This is NOT atomic as one unit!
// Another goroutine could read success and failure between these two operations
success.Add(1)
// ← another goroutine could snapshot here: success+1, failure+0 (inconsistent!)
failure.Add(-1)
// CORRECT: use a Mutex for operations that must be consistent
var mu sync.Mutex
var success2, failure2 int64
mu.Lock()
success2++
failure2--
mu.Unlock()
// ANTI-PATTERN: attempting a wrong lock-free implementation
var flag atomic.Bool
var data []byte
// Goroutine A:
flag.Store(true)
data = append(data, 1) // this is NOT guaranteed to be visible after flag.Store!
// Goroutine B:
if flag.Load() {
fmt.Println(data) // might see empty data!
}
// Memory ordering is more complex than this — use a channel or Mutex!
When to Switch to Alternatives #
Keep using sync/atomic if:
✓ Single counters: request counts, error counts, hit counts
✓ Boolean flags: shutdown, initialized, running
✓ Pointers replaced atomically: hot-reload configs
✓ Simple state machines with CAS
✓ Very performance-critical code with profiling
Use sync.Mutex if:
✗ You need to synchronize several variables at once
✗ Logic more complex than Load/Store/Add/CAS
✗ Not sure whether atomic is enough — Mutex is safer
Use sync.RWMutex if:
✗ Many readers, few writers, data spans more than one value
✗ Read operations need to read several fields at once
Use channels if:
✗ Communication between goroutines (sending data)
✗ Pipeline processing
✗ Fan-out / fan-in
✗ "Share memory by communicating" is more natural
Consider sync/atomic.Value for:
✗ Storing any value (structs, slices, maps) atomically
✗ Hot-reload configs that are frequently read
✗ Copy-on-write data structures
Summary #
- Use the new types in Go 1.19+:
atomic.Int64,atomic.Bool,atomic.Pointer[T]— more ergonomic, safer from type assertions, and no manual pointers needed.- Atomic is much faster than Mutex for single operations (~5ns vs ~25ns), but only for operations that genuinely can be done atomically — don’t force it if it doesn’t fit.
- Atomic for one value, Mutex for multiple values that must stay consistent — this is the most important rule. Two consecutive atomic operations are NOT one atomic operation.
atomic.Valuefor hot-reloading configs — store once, Load thousands of times without locks. The stored type must be consistent and must not be a nil interface.- CAS (CompareAndSwap) for state machines — only allow valid state transitions by guaranteeing the current state matches the expectation.
- Don’t use atomic to replace Mutex without understanding memory ordering — this is a source of very subtle, hard-to-debug bugs.
atomic.Int64.Add(-1)for decrements — there’s no Subtract operation; use Add with a negative value.- Always measure with benchmarks before replacing a Mutex with atomic — premature optimization is the root of all evil, and a wrong atomic is worse than a correct Mutex.
go test -raceis still needed even with atomic — the race detector can catch improper atomic usage.
← Previous: Flag