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:
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:
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
| Feature | Atomic | Mutex | Channel |
|---|
| Use case | Simple counters | Complex state | Communication |
| Performance | Fastest | Medium | Slowest |
| Complexity | Low | Medium | High |
| Lock-free | Yes | No | No |
| 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
- Use for simple state - Counters, flags, simple values
- Prefer typed atomics - Use
atomic.Int64 instead of atomic.AddInt64
- Always use atomics consistently - Don’t mix atomic and non-atomic ops
- Use CAS for complex updates - When you need to check before updating
- Consider mutexes for complex state - Don’t force atomics on complex data
- Profile before optimizing - Atomics are fast but may not be necessary
- Document atomic variables - Make it clear they’re accessed concurrently
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.