The Secret Life of Go: Atomic Operations
Source: Dev.to
The Indivisible Moment
Tuesday morning brought a sharp, biting wind off the Hudson, rattling the old sash windows of the library archive. Ethan arrived, shaking the cold from his coat, carrying a cardboard tray with two coffees and a small, wax‑paper bag.
Eleanor was already at the desk, her reading glasses perched on the end of her nose, reviewing a stack of printouts. She sniffed the air.
Eleanor: “Blueberry?”
Ethan: “Close. Huckleberry scones from a place in the West Village. And a Geisha pour‑over, black.”
Eleanor accepted the coffee with a rare, genuine smile.
Eleanor: “Excellent. You’re learning that precision matters in coffee just as much as in code.”
Ethan pulled up a chair.
Ethan: “Speaking of precision, I was thinking about last week. We fixed the data race in the counter using a Mutex. It worked, but…”
Eleanor: “But?” (she broke a piece of the scone)
Ethan: “It felt heavy. Locking, unlocking, deferring… just to add one to a number? Is there a faster way? Something… smaller?”
Eleanor wiped a crumb from her notebook.
Eleanor: “There is. But it requires you to think less like a programmer and more like a CPU.”
She opened her laptop.
Eleanor: “Remember when I told you
counter++isn’t one step? It’s three: read the value, add one, write it back. A Mutex protects all three steps by freezing the world around them. But modern processors have instructions that can do all three steps instantly—indivisibly.”
Ethan: “Indivisibly?”
Eleanor: “Atomically. From the Greek atomos—uncuttable. An operation that cannot be interrupted.”
Eleanor sketched a quick diagram in the margin of her notes.
NORMAL EXECUTION ATOMIC EXECUTION
[CPU Core 1] [CPU Core 1]
| |
| Read 0 | AtomicAdd(1)
| Add 1 | (Bus Locked 🔒)
| Write 1 | (Value becomes 1)
| | (Bus Unlocked 🔓)
[CPU Core 2] [CPU Core 2]
| |
| (Interferes!) | (Must Wait)
Eleanor: “See? In the atomic version, Core 2 physically cannot interfere. The hardware prevents it.”
Ethan nodded.
Ethan: “Okay, I see the theory. What does it look like in Go?”
Go Example – Atomic Add
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var counter int64
var wg sync.WaitGroup
start := time.Now()
// ... rest of the example ...
}
Eleanor: “No Mutex.
atomic.AddInt64uses that hardware instruction we just drew. It is the rawest form of synchronization.”
Ethan looked at the code.
Ethan: “It looks simple. Why don’t we use this for everything?”
Eleanor: “Because it only works for simple things—integers, pointers, booleans. You can’t atomically append to a slice or update a map. But for counters, gauges, or flags it’s orders of magnitude faster. A Mutex might cost you ~30 ns; an atomic add? One or two nanoseconds.”
Eleanor: “And it never sleeps. A Mutex involves the Go runtime scheduler—it might put a goroutine to sleep. An atomic operation happens entirely in hardware. It either succeeds or spins.”
She took a sip of the Geisha.
Eleanor: “There’s a catch, though. You have to read the data atomically, too. Printing
counterdirectly while others are writing can give you a ‘torn read’—half the bits from the old value, half from the new.”
Go Example – Atomic Load
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var value int64
// Writer
go func() {
for {
atomic.AddInt64(&value, 1)
time.Sleep(time.Millisecond)
}
}()
// Reader
for i := 0; i < 10; i++ {
fmt.Println(atomic.LoadInt64(&value))
time.Sleep(500 * time.Millisecond)
}
}
Eleanor: “Notice
atomic.LoadInt64. Just as you must write atomically, you must read atomically. This guarantees a consistent snapshot of memory.”
Ethan nodded slowly.
Ethan: “So,
Addto write,Loadto read. What else?”
Eleanor: “Store, and the most powerful one of all: Compare‑and‑Swap (CAS).”
Compare‑and‑Swap (CAS)
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var status int32 = 0 // 0 = idle, 1 = busy
// Try to set status to busy
swapped := atomic.CompareAndSwapInt32(&status, 0, 1)
if swapped {
fmt.Println("Success: Status changed from 0 to 1")
} else {
fmt.Println("Failure: Status was not 0")
}
// Try again
swapped = atomic.CompareAndSwapInt32(&status, 0, 1)
if swapped {
fmt.Println("Success: Status changed from 0 to 1")
} else {
fmt.Println("Failure: Status was already 1")
}
}
Output
Success: Status changed from 0 to 1
Failure: Status was already 1
Eleanor: “
CompareAndSwap—CAS for short—means: ‘I think the value is 0. If it is, change it to 1; if it isn’t, tell me.’ This lets you build lock‑free data structures and algorithms.”
The conversation continued, delving deeper into memory ordering, the atomic.Value type, and when to reach for a Mutex instead of an atomic operation. But that, dear reader, is a story for the next chapter.
Ethan, here is the rule:
• Use channels to orchestrate.
• Use Mutexes to protect complex state.
• Use atomics only when you need raw speed for simple counters or state flags.
“So, don’t be a hero?”
“Exactly. Clever code is hard to debug. Atomic code is impossible to debug.”
She tapped a final example into the laptop.
There is one high‑level atomic tool that is safe and incredibly useful:
atomic.Value.
It handles any type, not just numbers. It’s perfect for updating a configuration while the server is running.
package main
import (
"fmt"
"sync/atomic"
"time"
)
type Config struct {
APIKey string
Timeout time.Duration
}
func main() {
var config atomic.Value
// Initial config
config.Store(Config{APIKey: "INITIAL", Timeout: time.Second})
// Background updater
go func() {
for {
time.Sleep(100 * time.Millisecond)
// Atomically replace the entire struct
config.Store(Config{
APIKey: "UPDATED",
Timeout: 2 * time.Second,
})
}
}()
// Reader loop
for i := 0; i < 10; i++ {
cfg := config.Load().(Config)
fmt.Printf("APIKey=%s Timeout=%s\n", cfg.APIKey, cfg.Timeout)
time.Sleep(200 * time.Millisecond)
}
}
“See?
config.Load()returns the latest full configuration.config.Store()replaces it entirely. No locks, no blocking readers. The readers always see a complete, validConfigobject—either the old one or the new one, never a mix. Just be careful with that type assertion—atomic.Valuestores anany, so Go won’t check the type for you until runtime.”
Ethan finished his scone. “That’s actually really clean. atomic.Value for configuration, atomic.Add for metrics.”
“Precisely,” Eleanor closed her laptop. “You are building a toolbox, Ethan. Channels are your power drills. Mutexes are your vices. Atomics… they are the scalpel. You don’t use a scalpel to build a house, but when you need surgery, nothing else will do.”
She picked up her cup. The coffee was cooling, but she took a long, satisfied sip.
“Next time, we step back from the hardware. We need to talk about how to structure all this code—packages, visibility, and how to build a program that doesn’t collapse under its own weight.”
Ethan gathered the trash. “Project structure?”
“Project structure. The architecture of a Go application. It’s time you stopped writing scripts and started building systems.”
Key Concepts from Chapter 10
- Atomic Operations – Execute indivisibly; cannot be interrupted by other goroutines or CPU instructions. Found in the
sync/atomicpackage. atomic.AddInt64– Atomically increments a variable. Much faster than a Mutex for simple counters because it uses CPU hardware instructions rather than OS‑level locking.atomic.Load/atomic.Store– Safely read and write values without locks. Prevents “torn reads” where you might see partially written data.- Compare and Swap (CAS) –
atomic.CompareAndSwapInt32(&addr, old, new). Atomically updates a value only if it currently matches the “old” value. The basis for lock‑free algorithms. atomic.Value– Can atomically load and store values of any type (structs, maps, etc.). Ideal for “read‑heavy, write‑rarely” scenarios like updating global configuration.- Type Safety –
atomic.Valuestoresany(interface{}). Always verify your type assertions when loading; a mismatch will cause a runtime panic.
The Hierarchy of Synchronization
- Channels – Use for communication and passing ownership of data.
- Mutex – Use for protecting critical sections and complex shared state.
- Atomic – Use for simple counters, flags, and high‑performance metrics.
Memory Safety
- Do not mix atomic and non‑atomic access to the same variable.
- If you use
atomic.Store, you must useatomic.Loadto read it.
Next chapter: Project Structure and Packages – where Eleanor explains how to organize code, the internal directory, and how to design clean APIs.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.