Introduction
The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.This document version is from June 6, 2022. For the complete and most up-to-date memory model, see go.dev/ref/mem.
Advice
Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access. To serialize access, protect the data with channel operations or other synchronization primitives such as those in thesync and sync/atomic packages.
Informal Overview
Go approaches its memory model in much the same way as the rest of the language, aiming to keep the semantics simple, understandable, and useful. This section gives a general overview of the approach and should suffice for most programmers.Data Races
A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by thesync/atomic package.
Programmers are strongly encouraged to use appropriate synchronization to avoid data races. In the absence of data races, Go programs behave as if all the goroutines were multiplexed onto a single processor. This property is sometimes referred to as DRF-SC: data-race-free programs execute in a sequentially consistent manner.
- An implementation may always react to a data race by reporting the race and terminating the program
- Each read of a single-word-sized or sub-word-sized memory location must observe a value actually written to that location (perhaps by a concurrent executing goroutine) and not yet overwritten
Memory Model
The formal definition of Go’s memory model closely follows the approach presented by Hans-J. Boehm and Sarita V. Adve in “Foundations of the C++ Concurrency Memory Model”.Memory Operations
A memory operation is modeled by four details:- Its kind, indicating whether it is an ordinary data read, an ordinary data write, or a synchronizing operation such as an atomic data access, a mutex operation, or a channel operation
- Its location in the program
- The memory location or variable being accessed
- The values read or written by the operation
Some memory operations are read-like (read, atomic read, mutex lock, channel receive). Others are write-like (write, atomic write, mutex unlock, channel send, channel close). Some operations, such as atomic compare-and-swap, are both read-like and write-like.
Happens Before
The happens before relation is defined as the transitive closure of the union of the sequenced before and synchronized before relations. For an ordinary (non-synchronizing) data readr on a memory location x, W(r) must be a write w that is visible to r, where visible means that both of the following hold:
whappens beforerwdoes not happen before any other writew'(tox) that happens beforer
Data Races Defined
A read-write data race on memory locationx consists of:
- A read-like memory operation
ronx - A write-like memory operation
wonx - At least one of which is non-synchronizing
- Which are unordered by happens before
x consists of:
- Two write-like memory operations
wandw'onx - At least one of which is non-synchronizing
- Which are unordered by happens before
Implementation Restrictions for Programs Containing Data Races
Any implementation can, upon detecting a data race, report the race and halt execution of the program. Implementations using ThreadSanitizer (accessed withgo build -race) do exactly this.
A read of an array, struct, or complex number may be implemented as a read of each individual sub-value (array element, struct field, or real/imaginary component), in any order. Similarly, a write may be implemented as a write of each individual sub-value, in any order.
Synchronization
Initialization
Program initialization runs in a single goroutine, but that goroutine may create other goroutines, which run concurrently.If a package
p imports package q, the completion of q’s init functions happens before the start of any of p’s.init functions is synchronized before the start of the function main.main.
Goroutine Creation
The
go statement that starts a new goroutine is synchronized before the start of the goroutine’s execution.hello will print "hello, world" at some point in the future (perhaps after hello has returned).
Goroutine Destruction
For example, in this program:a is not followed by any synchronization event, so it is not guaranteed to be observed by any other goroutine. In fact, an aggressive compiler might delete the entire go statement.
Channel Communication
Channel communication is the main method of synchronization between goroutines. Each send on a particular channel is matched to a corresponding receive from that channel, usually in a different goroutine.Rule 1: Send Before Receive
A send on a channel is synchronized before the completion of the corresponding receive from that channel.
"hello, world". The write to a is sequenced before the send on c, which is synchronized before the corresponding receive on c completes, which is sequenced before the print.
Rule 2: Channel Close
The closing of a channel is synchronized before a receive that returns a zero value because the channel is closed.
c <- 0 with close(c) yields a program with the same guaranteed behavior.
Rule 3: Unbuffered Channels
A receive from an unbuffered channel is synchronized before the completion of the corresponding send on that channel.
"hello, world". The write to a is sequenced before the receive on c, which is synchronized before the corresponding send on c completes, which is sequenced before the print.
Locks
Thesync package implements two lock data types: sync.Mutex and sync.RWMutex.
For any
sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() is synchronized before call m of l.Lock() returns."hello, world". The first call to l.Unlock() (in f) is synchronized before the second call to l.Lock() (in main) returns, which is sequenced before the print.
Once
Thesync package provides a safe mechanism for initialization in the presence of multiple goroutines through the use of the Once type.
A single call of
f() from once.Do(f) is synchronized before the return of any call of once.Do(f).twoprint will call setup exactly once. The setup function will complete before either call of print. The result will be that "hello, world" will be printed twice.
Atomic Values
The APIs in thesync/atomic package are collectively “atomic operations” that can be used to synchronize the execution of different goroutines.
If the effect of an atomic operation
A is observed by atomic operation B, then A is synchronized before B. All the atomic operations executed in a program behave as though executed in some sequentially consistent order.Incorrect Synchronization
Programs with races can exhibit non-sequentially consistent executions. Be particularly careful with read-write races involving larger values.Example: Double-Checked Locking
This code is broken:This program is not guaranteed to print
"hello, world". It might print an empty string or never terminate. Use proper synchronization instead.Example: Incorrect Lazy Initialization
Another incorrect idiom is busy waiting for a value:Correct Synchronization Patterns
- Channels
- Mutex
- sync.Once
- Atomic
Key Takeaways
Avoid data races
Use proper synchronization primitives to protect shared data.
Use channels
Prefer channels for communication between goroutines.
Understand happens-before
Know the synchronization guarantees provided by Go.
Use sync package
Leverage sync.Mutex, sync.RWMutex, and sync.Once appropriately.
Test with -race
Use the race detector during development and testing.
Keep it simple
If you need to understand the memory model deeply, you’re being too clever.
Related Resources
Language Specification
Complete Go language reference
Effective Go
Best practices for concurrent programming
GODEBUG
Runtime debugging and compatibility settings