What does this print, and why? (copying a lock)
Question 224MediumGo 1.22 to 1.25
type Counter struct {
mu sync.Mutex
n int
}
func (c Counter) Inc() { // value receiver
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func main() {
var c Counter
var wg sync.WaitGroup
for range 100 {
wg.Go(c.Inc)
}
wg.Wait()
fmt.Println(c.n)
}
It prints 0. The value receiver copies the whole struct, including the mutex, so every call locks its own private mutex and increments its own copy of n.
Two more ways copying bites:
- Copying a mutex while it is locked produces a copy that is also locked. Any later
Lockon the copy deadlocks. - Locks get copied implicitly by
for _, v := range sliceOfStructs, by passing structs by value, and by returning them from functions.
Fix: use a pointer receiver func (c *Counter) Inc() and never copy a value after first use. This applies to Mutex, RWMutex, WaitGroup, Once, Cond, Pool, sync.Map and the typed atomics.
go vet's copylocks check catches most of these. Types that must not be copied embed a zero-size noCopy marker, and you can add the same marker to your own types.
More on Concurrency Patterns & sync
- Q222Explain sync.Mutex "normal mode" vs "starvation mode".
- Q223Is sync.Mutex reentrant? What happens with a recursive RLock on an RWMutex?
- Q225What is the classic sync.WaitGroup bug, and what does wg.Go (Go 1.25) change?
- Q226Explain sync.Once semantics, including panics, and the OnceFunc / OnceValue / OnceValues helpers.
- Q227When would you use sync.Cond, and why must Wait always be called in a loop?
- Q228How does sync.Pool work, how does it interact with the GC, and what are the common misuse pitfalls?