Skip to main content
The primary mechanism for managing state in Go is communication over channels. However, for simple state like counters, the sync/atomic package provides atomic operations that are faster than channels or mutexes.

Basic Atomic Counter

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	// Atomic unsigned integer counter
	var ops atomic.Uint64

	// WaitGroup to wait for goroutines
	var wg sync.WaitGroup

	// Start 50 goroutines
	for range 50 {
		wg.Go(func() {
			for range 1000 {
				// Atomically increment counter
				ops.Add(1)
			}
		})
	}

	// Wait for all goroutines
	wg.Wait()

	// Atomically read final value
	fmt.Println("ops:", ops.Load())
}
Output:
ops: 50000
Atomic operations guarantee that reads and writes happen as indivisible operations, preventing race conditions.

Atomic Types (Go 1.19+)

Go 1.19 introduced type-safe atomic types:
var counter atomic.Int64    // Signed integer
var ucounter atomic.Uint64  // Unsigned integer
var flag atomic.Bool        // Boolean
var ptr atomic.Pointer[T]   // Typed pointer
var value atomic.Value      // Any value

Atomic Operations

Add

Atomically add to a value:
var counter atomic.Int64
counter.Add(1)   // Increment by 1
counter.Add(10)  // Increment by 10
counter.Add(-5)  // Decrement by 5

Load

Atomically read a value:
value := counter.Load()
fmt.Println("Current value:", value)

Store

Atomically write a value:
counter.Store(42)

Swap

Atomically swap and return old value:
old := counter.Swap(100)
fmt.Println("Old value:", old)
fmt.Println("New value:", counter.Load())  // 100

CompareAndSwap (CAS)

Atomically compare and swap if equal:
var counter atomic.Int64
counter.Store(42)

// Swap only if current value is 42
swapped := counter.CompareAndSwap(42, 100)
fmt.Println("Swapped:", swapped)  // true
fmt.Println("Value:", counter.Load())  // 100

// Try to swap again - fails because value is now 100
swapped = counter.CompareAndSwap(42, 200)
fmt.Println("Swapped:", swapped)  // false
fmt.Println("Value:", counter.Load())  // 100 (unchanged)
CompareAndSwap is the foundation for lock-free algorithms and is extremely useful for implementing counters, flags, and state machines.

Atomic Boolean

var flag atomic.Bool

// Set to true
flag.Store(true)

// Check value
if flag.Load() {
	fmt.Println("Flag is set")
}

// Atomic swap
old := flag.Swap(false)
fmt.Println("Previous value:", old)  // true

// Compare and swap
if flag.CompareAndSwap(false, true) {
	fmt.Println("Successfully changed from false to true")
}

Atomic Pointer

type Config struct {
	Host string
	Port int
}

var config atomic.Pointer[Config]

// Store pointer
config.Store(&Config{Host: "localhost", Port: 8080})

// Load pointer
cfg := config.Load()
fmt.Printf("Config: %s:%d\n", cfg.Host, cfg.Port)

// Swap configuration atomically
oldCfg := config.Swap(&Config{Host: "example.com", Port: 443})
fmt.Printf("Old config: %s:%d\n", oldCfg.Host, oldCfg.Port)

Atomic Value (Any Type)

var state atomic.Value

// Store any value
state.Store("initial")

// Load and type assert
value := state.Load().(string)
fmt.Println("State:", value)

// Store different type (all stores must be same type)
state.Store("updated")
atomic.Value requires all stored values to be of the same type. Storing different types will panic.

Practical Examples

Request Counter

type Server struct {
	requests atomic.Uint64
	errors   atomic.Uint64
}

func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
	s.requests.Add(1)
	
	if err := s.processRequest(r); err != nil {
		s.errors.Add(1)
		http.Error(w, err.Error(), 500)
		return
	}
	
	w.WriteHeader(200)
}

func (s *Server) Stats() (requests, errors uint64) {
	return s.requests.Load(), s.errors.Load()
}

Connection Pool Counter

type ConnectionPool struct {
	active    atomic.Int32
	maxActive int32
}

func (cp *ConnectionPool) Acquire() (Connection, error) {
	for {
		current := cp.active.Load()
		if current >= cp.maxActive {
			return nil, errors.New("pool exhausted")
		}
		
		if cp.active.CompareAndSwap(current, current+1) {
			return cp.newConnection(), nil
		}
		// CAS failed, retry
	}
}

func (cp *ConnectionPool) Release(conn Connection) {
	conn.Close()
	cp.active.Add(-1)
}

Rate Limiter with Atomic Counter

type RateLimiter struct {
	count      atomic.Int64
	limit      int64
	lastReset  atomic.Int64  // Unix timestamp
	window     time.Duration
}

func (rl *RateLimiter) Allow() bool {
	now := time.Now().Unix()
	last := rl.lastReset.Load()
	
	// Reset if window expired
	if now-last >= int64(rl.window.Seconds()) {
		if rl.lastReset.CompareAndSwap(last, now) {
			rl.count.Store(0)
		}
	}
	
	// Increment and check
	new := rl.count.Add(1)
	return new <= rl.limit
}

Feature Flag Manager

type FeatureFlags struct {
	flags atomic.Value  // map[string]bool
	mu    sync.RWMutex
}

func (ff *FeatureFlags) Set(flags map[string]bool) {
	ff.flags.Store(flags)
}

func (ff *FeatureFlags) IsEnabled(feature string) bool {
	flags := ff.flags.Load().(map[string]bool)
	return flags[feature]
}

func (ff *FeatureFlags) UpdateFeature(name string, enabled bool) {
	ff.mu.Lock()
	defer ff.mu.Unlock()
	
	current := ff.flags.Load().(map[string]bool)
	updated := make(map[string]bool, len(current))
	for k, v := range current {
		updated[k] = v
	}
	updated[name] = enabled
	
	ff.flags.Store(updated)
}

Metrics Collector

type Metrics struct {
	requestCount    atomic.Uint64
	errorCount      atomic.Uint64
	totalDuration   atomic.Int64  // nanoseconds
	maxDuration     atomic.Int64  // nanoseconds
}

func (m *Metrics) RecordRequest(duration time.Duration) {
	m.requestCount.Add(1)
	m.totalDuration.Add(int64(duration))
	
	// Update max duration using CAS
	for {
		current := m.maxDuration.Load()
		if int64(duration) <= current {
			break
		}
		if m.maxDuration.CompareAndSwap(current, int64(duration)) {
			break
		}
	}
}

func (m *Metrics) RecordError() {
	m.errorCount.Add(1)
}

func (m *Metrics) Report() {
	count := m.requestCount.Load()
	errors := m.errorCount.Load()
	avg := time.Duration(m.totalDuration.Load() / int64(count))
	max := time.Duration(m.maxDuration.Load())
	
	fmt.Printf("Requests: %d, Errors: %d, Avg: %v, Max: %v\n",
		count, errors, avg, max)
}

Atomic vs Mutex vs Channel

FeatureAtomicMutexChannel
Use caseSimple countersComplex stateCommunication
PerformanceFastestMediumSlowest
ComplexityLowMediumHigh
Lock-freeYesNoNo
Overhead~1-5 ns~20-40 ns~50-100 ns
Use atomics for simple counters and flags. Use mutexes for protecting complex state. Use channels for communicating between goroutines.

Lock-Free Stack Example

type Node struct {
	value int
	next  *Node
}

type LockFreeStack struct {
	head atomic.Pointer[Node]
}

func (s *LockFreeStack) Push(value int) {
	node := &Node{value: value}
	
	for {
		old := s.head.Load()
		node.next = old
		
		if s.head.CompareAndSwap(old, node) {
			return
		}
		// CAS failed, retry
	}
}

func (s *LockFreeStack) Pop() (int, bool) {
	for {
		old := s.head.Load()
		if old == nil {
			return 0, false
		}
		
		if s.head.CompareAndSwap(old, old.next) {
			return old.value, true
		}
		// CAS failed, retry
	}
}

Common Pitfalls

Using Non-Atomic Operations

// BAD: Race condition
var counter int64
for i := 0; i < 100; i++ {
	go func() {
		counter++  // NOT SAFE!
	}()
}

// GOOD: Atomic operation
var counter atomic.Int64
for i := 0; i < 100; i++ {
	go func() {
		counter.Add(1)  // Safe
	}()
}

Mixing Atomic and Non-Atomic

// BAD: Mixing atomic and non-atomic
var counter atomic.Int64
counter.Store(10)
x := counter  // BAD: Direct access
x++           // BAD: Non-atomic operation

// GOOD: Always use atomic operations
var counter atomic.Int64
counter.Store(10)
old := counter.Load()
counter.Store(old + 1)
// Or better:
counter.Add(1)

Wrong Type for atomic.Value

// BAD: Changing types
var v atomic.Value
v.Store(42)        // int
v.Store("hello")   // PANIC: string (different type)

// GOOD: Consistent type
var v atomic.Value
v.Store(42)
v.Store(100)  // OK: same type

Best Practices

  1. Use for simple state - Counters, flags, simple values
  2. Prefer typed atomics - Use atomic.Int64 instead of atomic.AddInt64
  3. Always use atomics consistently - Don’t mix atomic and non-atomic ops
  4. Use CAS for complex updates - When you need to check before updating
  5. Consider mutexes for complex state - Don’t force atomics on complex data
  6. Profile before optimizing - Atomics are fast but may not be necessary
  7. Document atomic variables - Make it clear they’re accessed concurrently

Performance Benchmarks

func BenchmarkAtomicAdd(b *testing.B) {
	var counter atomic.Int64
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			counter.Add(1)
		}
	})
}

func BenchmarkMutexAdd(b *testing.B) {
	var (
		counter int64
		mu      sync.Mutex
	)
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			mu.Lock()
			counter++
			mu.Unlock()
		}
	})
}

// Results (approximate):
// BenchmarkAtomicAdd-8    1000000000    1.2 ns/op
// BenchmarkMutexAdd-8      50000000    30.0 ns/op
Atomic operations are typically 20-30x faster than mutex-protected operations for simple increments.

Build docs developers (and LLMs) love