Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go Memory Model

Overview

Go’s memory model defines the conditions under which reads of a variable in one goroutine are guaranteed to observe writes to the same variable in another goroutine. Understanding this is crucial for writing correct concurrent programs.

Happens-Before Relationships

An event e1 happens-before event e2 if e1 is ordered before e2 and e1’s effects are visible to e2.

Guaranteed Happens-Before

  1. Within a goroutine — Statements execute in program order
  2. Channel operations — Send happens-before corresponding receive completes
  3. Mutex operationsUnlock happens-before subsequent Lock
  4. sync.Oncef() invocation happens-before any Do(f) call returns
  5. sync.WaitGroupWait returns after all Done calls
  6. sync/atomic — Atomic operations provide sequential consistency

Channel Synchronization

var data int
var done = make(chan bool)

// Goroutine 1
go func() {
    data = 42          // (1) Write
    done <- true       // (2) Send
}()

// Goroutine 2
<-done                 // (3) Receive — happens after (2)
fmt.Println(data)      // (4) Guaranteed to see 42

The send on done happens-before the receive, establishing a happens-before from (1) to (4).

Mutex Synchronization

var mu sync.Mutex
var balance int

// Goroutine 1
mu.Lock()
balance = 100          // (1) Write
mu.Unlock()            // (2) Unlock

// Goroutine 2
mu.Lock()              // (3) Lock — happens after (2)
fmt.Println(balance)   // (4) Guaranteed to see 100
mu.Unlock()

Data Races

A data race occurs when two goroutines access the same variable concurrently and at least one is a write.

// DATA RACE!
var counter int

for i := 0; i < 1000; i++ {
    go func() {
        counter++ // Read + Write without synchronization
    }()
}

Detecting Data Races

go run -race main.go
go test -race ./...

The race detector uses ThreadSanitizer and adds ~10x overhead but catches races at runtime.

Fixing Data Races

// Fix 1: Mutex
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

// Fix 2: Atomic
var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

// Fix 3: Channel
var counter int64
var ops = make(chan struct{})

// Single goroutine owns the counter
go func() {
    for range ops {
        counter++
    }
}()

sync Package

PrimitiveUse CasePerformance
MutexMutual exclusionGood for short critical sections
RWMutexMultiple readers, single writerBetter for read-heavy workloads
WaitGroupWait for goroutines to finishLightweight
OnceOne-time initializationVery fast after first call
CondWait for conditionComplex, rarely needed
MapConcurrent mapGood for read-heavy, stable key sets
PoolObject reuseReduces GC pressure
atomicLow-level atomic opsFastest

sync.Once

var once sync.Once
var instance *Database

func GetDatabase() *Database {
    once.Do(func() {
        instance = connectToDatabase()
    })
    return instance
}

sync.Pool

var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process(data []byte) {
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf)
    buf.Reset()
    buf.Write(data)
    // use buf...
}

Interview Questions

Q: What is a data race?

A: When two goroutines access the same variable concurrently, at least one is a write, and there’s no synchronization. The Go runtime’s race detector can find these. Data races lead to undefined behavior.

Q: Mutex vs Channel for synchronization?

A: Use mutexes to protect shared state (simple read/write). Use channels to communicate between goroutines (transfer ownership, coordinate). Channels are higher-level and often clearer, but mutexes are faster for simple cases.

Q: What does the race detector do?

A: It instruments code at runtime to detect unsynchronized concurrent memory access. It uses happens-before analysis. It adds ~10x slowdown and ~5-10x memory overhead. Enable with -race flag.

Q: How does sync.Pool reduce GC pressure?

A: Objects in Pool are reused instead of allocated fresh each time. The pool is cleared during GC, but between GC cycles, objects are reused. This reduces allocation rate and thus GC work. Good for temporary buffers.