sync: WaitGroup & Mutex
Prerequisites: select
Channels are one way to coordinate goroutines. The sync package offers the
lower-level tools:
sync.WaitGroupwaits for a group of goroutines to finish.Add(n)increases the counter,Done()decreases it,Wait()blocks until it reaches zero.sync.Mutex(mutual exclusion lock) protects shared data. Only one goroutine can hold the lock at a time; the others wait inLock().sync.RWMutexallows many simultaneous readers (RLock) or one writer (Lock) — useful for read-heavy data.sync.Onceruns a function exactly once, even when called from many goroutines.
Rule of thumb: use channels to pass ownership of data between goroutines; use a mutex to protect a piece of state that several goroutines update.
Syntax
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
wg.Wait()
var mu sync.Mutex
mu.Lock()
// read or modify shared state
mu.Unlock()
Example
package main
import (
"fmt"
"sync"
)
// SafeCounter is safe to use from many goroutines.
type SafeCounter struct {
mu sync.Mutex
counts map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counts[key]++
}
func (c *SafeCounter) Get(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.counts[key]
}
func main() {
c := SafeCounter{counts: make(map[string]int)}
var wg sync.WaitGroup
for range 1000 {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc("hits")
}()
}
wg.Wait()
fmt.Println(c.Get("hits"))
var once sync.Once
for range 3 {
once.Do(func() { fmt.Println("initialised once") })
}
}
Expected output
1000
initialised once
Without the mutex, this program has a data race. Plain map writes from many
goroutines usually crash with fatal error: concurrent map writes, and a
plain int counter would often end up below 1000. go run -race . reports
the race either way.
Common mistakes
- Copying a struct that contains a
sync.Mutex(for example, a value receiver onSafeCounter). Each copy has its own lock, so nothing is protected. Use pointer receivers;go vetwarns about copied locks. - Forgetting to unlock on an early
return. Putdefer mu.Unlock()right aftermu.Lock(). - Locking only the writes and not the reads. Every access to shared state must hold the lock.
- Calling
Locktwice from the same goroutine. Go's mutexes are not re-entrant, so this deadlocks.
Practice
Create an Account with a mutex-protected balance int and a
Deposit(amount int) method. Start 50 goroutines that each deposit 10,
wait for all of them, and print the balance.
Show solution
package main
import (
"fmt"
"sync"
)
type Account struct {
mu sync.Mutex
balance int
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
}
func (a *Account) Balance() int {
a.mu.Lock()
defer a.mu.Unlock()
return a.balance
}
func main() {
acc := &Account{}
var wg sync.WaitGroup
for range 50 {
wg.Add(1)
go func() {
defer wg.Done()
acc.Deposit(10)
}()
}
wg.Wait()
fmt.Println(acc.Balance()) // 500
}