TL;DR
In Go, the atomic package works on pointers, not on copies. It protects a specific memory address but does not act like a higher-level lock.
Why Atomic Is Needed
Adding to a shared variable from many goroutines without atomic means each goroutine reads, increments, and writes back the same old value. The result depends on timing instead of intent.
Example (Race Condition)
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // not thread-safe!
}()
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
Expected: 1000 Actual (non-deterministic): something less, due to concurrent writes.
How Atomic Works
With atomic, operations target the memory address directly, and hardware instructions guarantee the update completes without interruption.
Example (Using Atomic)
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1) // atomic operation on pointer
}()
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
Now the output is always 1000 because atomic.AddInt64 updates the shared variable safely.
Be Careful When Using Atomic
atomic protects individual variables, not multi-step logic. When you need to guard a sequence of reads and writes, reach for sync.Mutex.
Appendix
Further reading: Go Concurrency and Atomics by Anton Zhiyanov and the official sync/atomic docs.