tezvyn:

Go's Memory Model: Don't Be Clever

AI-drafted, machine-checkedSource: go.devadvanced

Go guarantees your program behaves predictably—as if on one CPU—if you prevent data races. Use channels or `sync` primitives to serialize access when goroutines share data. The footgun is relying on timing instead of explicit synchronization.

WHY IT EXISTS The Go memory model specifies the conditions under which a read of a variable in one goroutine is guaranteed to observe a value written by a different goroutine. Without a formal model, compiler and hardware optimizations could reorder memory operations, leading to unpredictable behavior in concurrent programs. Go's model prioritizes simplicity and safety over the undefined behavior found in languages like C++.

THE MENTAL MODEL Think "happens-before." A write to a variable in one goroutine is not magically visible to all others. An explicit synchronization action, like a channel send/receive or a mutex unlock/lock, creates a happens-before relationship. This guarantees that all memory operations before the action are visible to code that runs after the corresponding synchronizing action in another goroutine. If no happens-before path exists between a write and a read of the same variable, you have a data race.

HOW IT WORKS Go provides a powerful guarantee: Data-Race-Free programs execute in a Sequentially Consistent manner (DRF-SC). This means if you correctly use synchronization to avoid data races, your program behaves as if all goroutines were simply interleaved on a single processor, which is much easier to reason about. A data race occurs when two goroutines access the same memory location concurrently, at least one access is a write, and the accesses are not protected by synchronization primitives like channels, mutexes (sync package), or atomic operations (sync/atomic package).

WHEN TO USE IT You must consider the memory model whenever goroutines access shared, mutable state. If goroutine A writes to a variable x and goroutine B reads from x, you must use synchronization to ensure the read cannot happen before the write. If you don't, you have a data race, and the read in B might see a stale value, the new value, or even a partially written value if the data is larger than a single machine word.

WHEN NOT TO USE IT The best way to avoid memory model complexity is to avoid sharing memory. The Go proverb is "Do not communicate by sharing memory; instead, share memory by communicating." If each piece of data is owned by a single goroutine, and updates are passed between goroutines via channels, you eliminate most data races by design.

ONE CANONICAL EXAMPLE Imagine one goroutine sets a variable a and then a boolean flag done. A second goroutine spins, waiting for done to become true before it reads a. This is a classic data race. There's no guarantee that the write to a will be visible to the second goroutine when it sees done as true; the compiler or CPU might reorder the writes. The correct Go approach is to use a channel. The first goroutine writes to a and then sends a value on a channel. The second goroutine waits to receive from that channel. This channel operation establishes the necessary happens-before relationship, guaranteeing the write to a is visible.

Read the original → go.dev

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.