sync.Mutex
Let only one goroutine touch shared data at a time, and keep the lock in the same struct as the data it guards.
var mu sync.Mutex; mu.Lock(); defer mu.Unlock()A goroutine is a function that runs alongside the rest of your program. When two goroutines change the same variable at the same moment, the result is undefined: a count can be lost, a map can be left broken. That is a data race. A sync.Mutex prevents it. Mutex is short for mutual exclusion. Only one goroutine may hold it at a time.
How it works
Lock takes the lock. If another goroutine already holds it, Lock waits there until the lock is free. Unlock releases it. The lines between the two calls are the critical section, and exactly one goroutine runs them at a time. The zero value of a Mutex is ready to use, so var mu sync.Mutex needs no setup and no new.
Keep the lock next to the data. Put the mutex in the same struct as the fields it protects, on the line above them. Then anyone reading the code can see which lock covers which field.
What trips people up
Write defer mu.Unlock() on the line after mu.Lock(). defer runs when the function returns, including early returns and panics, so the lock cannot be left held. The longhand is an Unlock call before every return. Same effect, more places to forget one.
Unlocking a Mutex that is not locked is a run-time error. Locking the same Mutex twice in one goroutine deadlocks: it waits forever for itself. A Mutex must never be copied, so pass the struct holding it by pointer.
Example
package main
import (
"fmt"
"sync"
)
// counter keeps the lock on the line above the number it guards.
type counter struct {
mu sync.Mutex
n int
}
func (c *counter) add(delta int) {
// Lock blocks until no other goroutine holds the lock.
c.mu.Lock()
// defer releases it when add returns, on every path out.
defer c.mu.Unlock()
c.n += delta
}
func (c *counter) value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
c := &counter{}
var wg sync.WaitGroup
for range 100 {
// wg.Go (Go 1.25 and later) is shorthand for:
// wg.Add(1); go func() { defer wg.Done(); c.add(1) }()
wg.Go(func() { c.add(1) })
}
wg.Wait()
fmt.Println("total:", c.value())
}
// Output:
// total: 100When to use it
- Keeping an in-process request counter or set of statistics correct while many HTTP handlers update it.
- Guarding a map of connected clients so one goroutine can add a client while another sends to all of them.
- Making a type that holds shared configuration safe to call from any goroutine, without the caller needing to know.
More examples
defer Unlock, and the same function written longhand
These two methods do the same thing. The short form is what you will write; the long form is what you will meet in older code, and in the rare function that must release the lock before doing something slow at the end. Read the long form carefully: add a third return later and you must remember a third Unlock.
// Short form: defer releases the lock however the function ends.
func (c *counter) add(delta int) {
c.mu.Lock()
defer c.mu.Unlock()
if delta == 0 {
return
}
c.n += delta
}
// Long form: every exit unlocks for itself. Identical behaviour.
func (c *counter) addLonghand(delta int) {
c.mu.Lock()
if delta == 0 {
c.mu.Unlock()
return
}
c.n += delta
c.mu.Unlock()
}A map behind a mutex
A Go map cannot be written by two goroutines at once; the runtime may stop the program with a "concurrent map writes" error. Wrapping it in a small type with its own mutex is the standard fix. Note that get locks too: reading a map while another goroutine writes it is also a race. The comment "guarded by mu" is a convention worth copying, because it tells the next reader which lock applies.
type sessions struct {
mu sync.Mutex
m map[string]string // guarded by mu
}
func (s *sessions) put(id, user string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.m == nil {
s.m = make(map[string]string)
}
s.m[id] = user
}
func (s *sessions) get(id string) (string, bool) {
s.mu.Lock()
defer s.mu.Unlock()
user, ok := s.m[id]
return user, ok
}TryLock to skip work someone else is already doing
<code>TryLock</code> reports whether it got the lock instead of waiting for it. Reach for it when a second attempt at the same job is pointless, such as rebuilding a cache that one goroutine is already rebuilding. Do not use it to avoid thinking about lock ordering: if your code only works because TryLock sometimes fails, the design is wrong.
type refresher struct {
mu sync.Mutex
}
// refresh returns false if another goroutine is already refreshing.
func (r *refresher) refresh() bool {
// TryLock was added in Go 1.18. It never blocks.
if !r.mu.TryLock() {
return false
}
defer r.mu.Unlock()
rebuildIndex() // defined elsewhere in the project
return true
}
Discussion