liveBook · Manning

Synchronization

As explained in 100 Go Mistakes and How to Avoid Them

Synchronization is the coordination of concurrent execution so that access to shared state occurs according to required ordering or exclusivity constraints.

A channel does not automatically make goroutines meet at a known point, and a mutex does not tell a test that background work has finished. Synchronization depends on the guarantee needed: exclusive access to shared state or a rendezvous between goroutines. section 8.3 section 9.7

Choose the mechanism by the interaction:

  • When parallel goroutines access or mutate a shared resource, use a mutex to keep them out of the critical section at the same time. section 8.3
  • When one stage must signal another or transfer ownership of a ready resource, use a channel. An unbuffered send waits for a receiver, giving both goroutines a rendezvous. section 8.3 section 9.7
  • Do not treat a buffered send as a rendezvous. It can complete while space remains, without a receiver being ready; it blocks only when the buffer is full. section 9.7
ch3 := make(chan int, 1)
ch3 <-1                   #1
ch3 <-2                   #2

With capacity one, the first send can proceed without a receiver. The second blocks because the buffer is full—not because every send synchronizes with a receive. section 9.7

An asynchronous test that sleeps

A handler starts a goroutine that calls a publisher. A mock can protect its recorded value with sync.RWMutex, yet a test that sleeps briefly before reading it may still run before publication. The lock protects access; the sleep does not establish completion. Instead, have the mock send the published value on an unbuffered channel and let the test receive before asserting. A select with time.After can bound the wait if publication fails. If direct synchronization is unavailable, retrying the assertion is preferable to one passive sleep. section 11.5

type publisherMock struct {
    ch chan []Foo
}
 
func (p *publisherMock) Publish(got []Foo) {
    p.ch <- got                               #1
}
 
func TestGetBestFoo(t *testing.T) {
    mock := publisherMock{
        ch: make(chan []Foo),
    }
    defer close(mock.ch)
 
    h := Handler{
        publisher: &mock,
        n:         2,
    }
    foo := h.getBestFoo(42)
    // Check foo
 
    if v := len(<-mock.ch); v != 2 {          #2
        t.Fatalf("expected 2, got %d", v)
    }
}

The publisher mock sends the received slice to the test, which waits for it before checking its length. section 11.5

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Defines synchronization through observable guarantees and contrasts channel buffering, but does not work through memory-model rules.
Examples
Traces sends to a capacity-one channel and contrasts a sleeping publisher test with a channel-backed mock.
What it assumes
Assumes familiarity with goroutines and basic Go channels while defining synchronization by the guarantees each mechanism provides.
What you can do after
Apply it via Go mutexes or unbuffered channels to protect shared resources or wait deterministically for publication.
How it connects
Relates synchronization to parallelism, concurrency, mutexes, channels, data races, race conditions, and the Go memory model.

Also explained in

Compare how 11 books explain synchronization →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.