Go by Example: Mutexes
Safely access shared state across multiple goroutines using mutual exclusion locks. This example demonstrates using `sync.Mutex` to prevent race conditions when incrementing a shared counter.
Code
package main
import (
"fmt"
"sync"
)
type Container struct {
mu sync.Mutex
counters map[string]int
}
func (c *Container) inc(name string) {
// Lock the mutex before accessing counters
c.mu.Lock()
// Ensure the mutex is unlocked when the function exits
defer c.mu.Unlock()
c.counters[name]++
}
func main() {
c := Container{
counters: map[string]int{"a": 0, "b": 0},
}
var wg sync.WaitGroup
// Increment a named counter in a loop
doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
wg.Done()
}
wg.Add(3)
go doIncrement("a", 10000)
go doIncrement("a", 10000)
go doIncrement("b", 10000)
wg.Wait()
fmt.Println(c.counters)
}Explanation
In concurrent programming, race conditions occur when multiple goroutines access shared memory simultaneously without synchronization. Go's sync.Mutex (Mutual Exclusion lock) provides a way to ensure that only one goroutine can access a critical section of code at a time. By explicitly locking and unlocking the mutex, you protect shared resources (like maps, slices, or counters) from corruption.
Using Mutex correctly involves wrapping sensitive operations between Lock() and Unlock() calls. A common idiom is to use defer mu.Unlock() immediately after locking to guarantee the lock is released even if the function panics or returns early. While channels are often preferred for passing data, Mutexes are the standard tool for managing shared state in structs.
Important considerations:
- Critical Section: The code between Lock and Unlock should be as short as possible to minimize contention.
- Deadlocks: Forgetting to unlock or locking the same mutex twice in the same goroutine will cause the program to hang.
- Pass by Pointer: Mutexes must not be copied; always pass them by pointer or embed them in a struct passed by pointer.

