Irbisa · Cheatsheet
sync, Atomics & the Memory Model
Mutexes, sync.Once, atomics, happens-before, the race detector
Which synchronisation primitives does the
syncpackage give you, and what is each one for?What is wrong with this
WaitGroupcode, and how doeswg.Gofrom Go 1.25 rule the bug out?func fetchAll(ids []int) []Row { var wg sync.WaitGroup rows := make([]Row, len(ids)) for i, id := range ids { go func() { wg.Add(1) defer wg.Done() rows[i] = load(id) }() } wg.Wait() return rows}Why is
defer mu.Unlock()the default right aftermu.Lock(), and when should you unlock explicitly instead?// wrong: the cache-hit path returns with the mutex still heldfunc (c *Cache) Get(k string) ([]byte, error) { c.mu.Lock() if v, ok := c.m[k]; ok { return v, nil // never unlocked: every later Get blocks forever }…What is the difference between a deadlock, a livelock and starvation?
IncrcallsGetwhile holding the same mutex. What happens at runtime, and why does Go have no reentrant mutex?type Cache struct { mu sync.Mutex m map[string]int}func (c *Cache) Get(k string) int { c.mu.Lock() defer c.mu.Unlock() return c.m[k]} func (c *Cache) Incr(k string) int { c.mu.Lock() defer c.mu.Unlock() c.m[k]++ return c.Get(k)}Find the bug: after one client stops reading, the whole chat hub freezes; new
Subscribecalls hang, and so does every otherPublish.type Hub struct { mu sync.Mutex subs map[int]chan string} func (h *Hub) Subscribe(id int) <-chan string { h.mu.Lock() defer h.mu.Unlock() ch := make(chan string) h.subs[id] = ch return ch} func (h *Hub) Publish(msg string) { h.mu.Lock() defer h.mu.Unlock() for _, ch := range h.subs { ch <- msg }}How do you build a thread-safe lazy singleton in Go, and what does
sync.Onceguarantee that anif instance == nilcheck does not?// wrong: two goroutines can both see nil, and the read is a data racevar client *http.Client func Client() *http.Client { if client == nil { client = newClient()…What is the difference between a data race and a race condition, and why can
-racestay green while a mutex-guarded balance goes negative?// wrong: -race is clean, every access is locked, and it is still brokenfunc (a *Account) Withdraw(n int64) error { if a.Balance() < n { // RLock ... RUnlock return ErrInsufficient } a.add(-n) // Lock ... Unlock: a *different* critical section…Why is an unsynchronised
boolstop flag a bug, even if "at worst a goroutine reads a stale value" and a single byte cannot tear?// wrong: "it is just a bool"; there is no such thing as a benign racevar ready boolvar payload *Report func producer() { payload = build(); ready = true } // two plain stores, nothing orders them…What does
sync/atomicgive you, and when would you use an atomic instead of aMutex?var hits atomic.Int64var ready atomic.Boolvar cfg atomic.Pointer[Config] hits.Add(1) // counter: read-modify-write in one stepready.Store(true) // flag: visible to every later Load…This gauge should keep the highest value seen. Every access is atomic and
-raceis clean, yet the reported maximum is sometimes too low. Why, and what is the fix?type Gauge struct{ max atomic.Int64 } func (g *Gauge) Observe(n int64) { if g.max.Load() < n { g.max.Store(n) }}What is the difference between
sync.Mutexandsync.RWMutex, and why can switching toRWMutexmake a read-heavy map slower?How does
sync.Mutexwork inside, and what are its normal and starvation modes?What is
sync.Map, and when is it better or worse than a plainmapbehind aMutex?// right use: written once, read forever, e.g. a registry of compiled codecsvar codecs sync.Map // string -> Codec func codecFor(name string) (Codec, error) { if v, ok := codecs.Load(name); ok { return v.(Codec), nil // every read pays a type assertion…How does the race detector work, and what can it not find?
// The detector sees only what runs, so make the test run the overlap.func TestLimiterConcurrent(t *testing.T) { l := NewLimiter(100) var wg sync.WaitGroup for range 64 { // one goroutine would find nothing wg.Go(func() {…How does
sync.Condwork, and why do most Go codebases use a channel instead?// before: Cond cannot be selected on, so no timeout and no shutdown wake-upfunc (q *CondQueue) Pop() Job { q.mu.Lock() defer q.mu.Unlock() for len(q.jobs) == 0 { // `for`, never `if`: the predicate may be false q.cond.Wait() // again once Wait has re-taken the lock…Find the bug: a health checker that ranges over
Snapshot()sometimes crashes withconcurrent map iteration and map write.type Registry struct { mu sync.RWMutex peers map[string]Peer} func (r *Registry) Add(p Peer) { r.mu.Lock() defer r.mu.Unlock() r.peers[p.ID] = p} func (r *Registry) Snapshot() map[string]Peer { r.mu.RLock() defer r.mu.RUnlock() return r.peers}How does
sync.Poolreduce allocations, and when does it hurt instead of helping?var bufs = sync.Pool{New: func() any { return new(bytes.Buffer) }} func render(w io.Writer, v any) error { b := bufs.Get().(*bytes.Buffer) b.Reset() // reset on the way IN: Get may return a used one…What does happens-before mean in the Go memory model, and which operations create it?
// Each pair is free of data races: the marked operation is the edge.var ready = make(chan struct{}) func publishByChannel() { cfg = load(); close(ready) } // close ...func readByChannel() { <-ready; use(cfg) } // ... before this receive…Why is double-checked locking on a plain pointer broken in Go, and how do you hot-swap a config that every request reads?
// wrong: textbook double-checked locking, a data race in Govar cfg *Configvar mu sync.Mutex func Get() *Config { if cfg == nil { // unsynchronised read…Calls to
transfer(a, b)andtransfer(b, a)hang forever while the rest of the server keeps working. What is the bug, and how do you make it impossible?// wrong: two lock orders form a cycle; transfer(x, y) and transfer(y, x) hangfunc transfer(a, b *Account, n int64) { a.mu.Lock() defer a.mu.Unlock() b.mu.Lock() defer b.mu.Unlock()…Per-shard counters in a
[]struct{ n atomic.Int64 }get slower per operation as cores are added, though no two goroutines touch the same element. Why?// wrong: 8 bytes each, so eight of these share one 64-byte cache linetype packed struct{ n atomic.Int64 } // right: one line each. 128, not 64: internal/cpu.CacheLinePadSize is 128// on arm64, and sync.Pool pads its per-P struct to 128 for the same reason.type padded struct {…