Skip to content

04 — The sync package

Channels are for goroutines that hand each other work and results. The sync package is for the other case: several goroutines reading and writing the same variable. The Go memory model ↗ states the rule in one sentence: a program whose goroutines modify data that others are accessing at the same time must serialize that access, with channel operations or with the primitives in sync and sync/atomic. This chapter covers those primitives and the rules that come with each.

A mutex is often the simpler choice. A counter, a cache, or a struct that goroutines read and update is state. The Go wiki ↗ lists state and caches under the mutex. Passing ownership of data, handing out work and delivering results go under channels. Its advice is to use whichever is more expressive or simpler. A Mutex around three lines beats a goroutine that owns a map plus a channel protocol to reach it. If the locking rules grow too tangled to keep in your head, that is when to reconsider channels.

Lesson The one thing
A mutex guards a counter goroutines adding to one int lose increments; with a sync.Mutex around the increment they lose none
Atomic counters atomic.Int64 counts exactly with no lock, and CompareAndSwap lets exactly one goroutine win
sync.Once runs exactly once ten simultaneous callers, one run; a panic is forgotten by Once and repeated by OnceFunc
A WaitGroup counts goroutines Add goes before the go statement, because an Add inside the goroutine can come after Wait has returned

Planned

  • sync.RWMutex: many readers or one writer, and how to tell whether that beats a plain Mutex for a given workload.
  • sync.Cond: waiting for a condition, and the channel that usually replaces it.
  • sync.Map: the two access patterns it is built for, and a map with a Mutex for everything else.
  • sync.Pool: reusing allocations the garbage collector is free to discard.
  • Copying a lock: go vet's copylocks check on a struct holding a Mutex passed by value.