sync.Mutex and sync.RWMutex in Go
Protect shared data from concurrent goroutines with sync.Mutex, allow many readers with sync.RWMutex, and avoid the usual locking mistakes.
When two goroutines change the same variable at the same time, the result can be wrong in ways that are hard to reproduce. A mutex (short for "mutual exclusion") is a lock that lets only one goroutine at a time run a piece of code. Go provides it as sync.Mutex.
The Problem a Mutex Solves
count++ looks like one step, but the CPU does three: read the value, add one, write it back. If two goroutines read the same old value before either writes, one increment is lost.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
var c Counter
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println("count:", c.Value())
}
This always prints count: 1000. Between Lock and Unlock, only one goroutine can be inside. Others that call Lock wait their turn. Without the mutex, the result would often be a smaller number, different on each run.
The code between Lock and Unlock is called the critical section. Keep it as short as possible, because every other goroutine that needs the lock waits while it runs.
Keep the Mutex Next to the Data It Protects
The usual Go style puts the mutex inside the struct, right above the fields it guards, and only touches those fields through methods that lock. Callers never see the mutex, so they cannot forget it.
Copying a struct that contains a mutex:
Methods on a struct with a mutex must use a pointer receiver, func (c *Counter). With a value receiver, every call locks a copy of the mutex, so nothing is protected. go vet reports this as Inc passes lock by value: main.Counter contains sync.Mutex.
Protecting a Map
Maps are not safe for concurrent writes. Go detects this at run time and stops the program with fatal error: concurrent map writes, which cannot be recovered. Wrap the map with a mutex:
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.Mutex
data map[string]string
}
func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[k] = v
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
v, ok := c.data[k]
return v, ok
}
func main() {
c := NewCache()
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Set(fmt.Sprint("key", i), "value")
}()
}
wg.Wait()
v, ok := c.Get("key7")
fmt.Println(v, ok, len(c.data))
}
This prints value true 50. Every read and write goes through the lock.
sync.RWMutex for Read-Heavy Data
If many goroutines read and few write, a plain mutex makes readers wait for each other even though reading together is safe. sync.RWMutex has two kinds of lock:
RLock/RUnlock: many readers can hold it at the same time.Lock/Unlock: a writer gets exclusive access, with no readers or other writers.
package main
import (
"fmt"
"sync"
)
type Config struct {
mu sync.RWMutex
vals map[string]string
}
func (c *Config) Get(k string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.vals[k]
}
func (c *Config) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.vals[k] = v
}
func main() {
cfg := &Config{vals: map[string]string{"mode": "dev"}}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = cfg.Get("mode")
}()
}
cfg.Set("mode", "prod")
wg.Wait()
fmt.Println("mode:", cfg.Get("mode"))
}
The final line prints mode: prod. Readers share the read lock, and Set waits until they release it.
RWMutex is not always faster:
An RWMutex does more bookkeeping than a Mutex. For short critical sections or when writes are common, a plain Mutex is often just as fast and simpler. Reach for RWMutex when reads clearly dominate and hold the lock for a noticeable time.
Common Locking Mistakes
Forgetting to unlock on an early return. If a function returns between Lock and Unlock, the lock is never released, and the next Lock waits forever. defer mu.Unlock() right after Lock prevents this.
Locking twice in the same goroutine. Go's mutex is not reentrant. If a method holding the lock calls another method that also locks, the goroutine waits for itself forever:
func (c *Counter) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
if c.Value() > 0 { // Value also locks: deadlock
c.n = 0
}
}
Read c.n directly inside Reset, or split the logic into an unexported helper that assumes the lock is already held.
Unlocking a mutex that is not locked crashes with fatal error: sync: unlock of unlocked mutex.
Do not hold a lock while waiting:
Holding a mutex while doing a network call, a channel send or a long computation makes every other goroutine wait behind it. Copy what you need, unlock, then do the slow work.
Solid mutex usage:
The mutex lives in the struct next to the fields it guards, methods use pointer receivers, every Lock is followed by defer Unlock, and no slow work happens inside the lock.
Summary
sync.Mutex allows one goroutine at a time into a critical section, which keeps shared counters, maps and structs correct. Put the mutex inside the struct, use pointer receivers, and pair every Lock with defer Unlock. Use sync.RWMutex when many readers and few writers share data, and never lock the same mutex twice in one goroutine or hold it during slow operations.