Skip to main content
eLearner.app
Module 7 · Lesson 4 of 534/50 in the course~14 min
Module lessons (4/5)

`sync.Mutex` and `sync.WaitGroup`

Channels are Go's primary idiom, but sometimes more traditional primitives are needed. The sync package provides mutexes, wait groups and other utilities to coordinate shared state.

sync.Mutex: exclusive lock

Go
import "sync"

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

Only one goroutine at a time enters between Lock() and Unlock(). Mandatory pattern: defer mu.Unlock() right after Lock().

sync.RWMutex: many readers, one writer

For read-heavy state:

Go
var mu sync.RWMutex

mu.RLock()              // read lock: più goroutine simultanee OK
v := data
mu.RUnlock()

mu.Lock()               // write lock: esclusivo
data = newValue
mu.Unlock()

Useful only if reads really dominate over writes; otherwise a regular sync.Mutex is simpler and often faster thanks to lower internal contention.

sync.WaitGroup: wait for N goroutines

Go
var wg sync.WaitGroup

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        work(id)
    }(i)
}
wg.Wait()    // blocca finché tutti i Done() sono arrivati

Workflow:

  1. wg.Add(N) increments the counter by N (before launching the goroutines).
  2. Each goroutine calls wg.Done() when it finishes (ideally via defer).
  3. wg.Wait() blocks until the counter goes back to 0.

sync.Once: thread-safe lazy initialization

Go
var (
    once   sync.Once
    config *Config
)

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()
    })
    return config
}

The callback in once.Do runs exactly once, even under concurrent calls. Singleton / lazy initialization pattern.

Mutex vs channel: when to use what

Guidelines (also from the official Go FAQ):

CasePreferred tool
Passing ownership of datachannel
Distributing work (work queue)channel
Coordinating independent goroutineschannel
Protecting a struct fieldMutex
Cache / counterMutex or atomic
Singleton referencesync.Once

"Channels to orchestrate, mutexes for shared data" is a good rule of thumb.

Race detector

Run tests with -race to spot data races:

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

The race detector instruments the binary and logs every unsynchronized access to shared memory. It is tool #1 to validate concurrent code before production.

Try it

Exercise#go.m7.l4.e1
Attempts: 0Loading…

Protect the increment of count with mu.Lock() + defer mu.Unlock().

Loading editor…
Show hint

defer mu.Unlock() right after Lock() guarantees release even on panic.

Solution available after 3 attempts

Exercise#go.m7.l4.e2
Attempts: 0Loading…

Launch 3 goroutines and wait for all of them using sync.WaitGroup (Add(3), Done() in each goroutine, Wait()).

Loading editor…
Show hint

Add BEFORE launching the goroutine; Done INSIDE the goroutine with defer.

Solution available after 3 attempts

Quiz#go.m7.l4.e3
Ready

What is the recommended pattern to guarantee the mutex is released?

Go
mu.Lock()
// ?
Answer options

Recap

  • sync.Mutex protects shared state; always defer mu.Unlock().
  • Use a pointer receiver to avoid copying the mutex.
  • sync.RWMutex for many-readers/one-writer patterns (only if it helps).
  • sync.WaitGroup: Add before go, Done with defer, Wait to wait.
  • sync.Once: thread-safe lazy initialization.
  • "Channels to orchestrate, mutexes for shared data".
  • go test -race: tool #1 to spot data races.