A Go map shared by goroutines needs protection when they may access it concurrently. A mutex gives one goroutine exclusive access to the protected resource at a time. In a cache, that resource is the map holding entries; in an HTTP service, it can be the repository map shared by request handlers. section 7.3.4 section 8.4.3
Keep the lock’s responsibility narrow:
- Put an unexported sync.Mutex beside the field it protects, rather than locking the whole server. If the storage is already thread-safe, an additional mutex may not be needed. section 8.4.4
- Call Lock before accessing the field, then immediately arrange defer Unlock. Protect reads as well as writes, including a check followed by an insertion. section 7.6 section 8.4.4
- Pass a used mutex, or a struct containing one, by pointer: copying a mutex after first use can cause unexpected locking behavior. Its zero value is unlocked; TryLock is rarely the best synchronization choice. section 7.3.4
Listing 8.33 memory.go: Callilng the Lock and Unlock mutex on Add()
// Add inserts a game in memory the first time it is received.
func (gr *GameRepository) Add(game session.Game) error {
log.Print("Adding a game...")
// Lock the reading and the writing of the game.
gr.mutex.Lock() #1
defer gr.mutex.Unlock() #2
_, ok := gr.storage[game.ID]
if ok {
return fmt.Errorf("%w (%s)", ErrConflictingID, game.ID)
}
gr.storage[game.ID] = game
return nil
}
The repository’s Add method holds one lock across both the duplicate-ID check and insertion. Deferred unlocking also covers the early error return. section 8.4.4
Choose the lock to match what a method actually does. A cache Read that deletes expired entries writes to the map, so it uses sync.Mutex rather than the RWMutex previously used for reads. The tradeoff is that two such reads can no longer run simultaneously. section 7.4.1
Listing 7.14 cache.go: Read with a TTL
func (c *Cache[K, V]) Read(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
var zeroV V #1
e, ok := c.data[key]
switch {
case !ok:
return zeroV, false
case e.expires.Before(time.Now()):
// The value has expired.
delete(c.data, key)
return zeroV, false
default:
return e.value, true
}
}
This TTL cache read locks before looking up an entry and holds the lock through possible deletion of an expired entry. section 7.4.1
A solver can likewise lock a shared solution field when saving a result and when checking whether one has been found. That quick stop signal does not, by itself, reliably fix the solver’s deadlock. Test concurrent code with go test -race, but a run without a race report does not prove that no race exists. section 9.3.5 section 7.6

















