Menu

Golang Mutex: sync.Mutex, RWMutex, atomic and sync.Once

How to protect shared state between goroutines with sync.Mutex and sync.RWMutex, when sync/atomic is enough, how sync.Once runs setup exactly once, and the locking mistakes that cause deadlocks.

This page includes runnable editors - edit, run, and see output instantly.

Protecting shared data

A sync.Mutex allows one goroutine at a time into the code between Lock and Unlock. Put the mutex next to the data it guards, usually in the same struct:

This always prints hits: 10000. Without the lock, 100 goroutines writing the same map would usually crash the program with fatal error: concurrent map writes. That error comes from a best-effort check in the runtime, it cannot be recovered, and the lack of a crash does not prove the code is correct.

Details that matter:

  • The zero value of sync.Mutex is unlocked and ready. No constructor.
  • Methods use a pointer receiver (*Counter). A value receiver would lock a copy of the mutex, which protects nothing.
  • defer c.mu.Unlock() right after Lock means every return path, and a panic, releases the lock.
  • Every access goes through the lock, reads included. A read without the lock while another goroutine writes is still a data race.

Keep the critical section small

defer unlocks at the end of the function. That is right for short methods like the ones above. In a longer function, unlock as soon as the shared data is no longer touched, so other goroutines are not waiting on work that does not need the lock:

func (s *Store) Save(key string) error {
	s.mu.Lock()
	data := s.items[key] // copy what you need
	s.mu.Unlock()

	return writeToDisk(key, data) // slow I/O, outside the lock
}

Holding a lock across network calls, disk I/O or a channel send is the most common cause of a slow concurrent program, and a channel send under a lock is a common cause of deadlock.

RWMutex for read-heavy data

sync.RWMutex has two modes. RLock/RUnlock take a shared read lock that many goroutines can hold at once. Lock/Unlock take the exclusive write lock, which waits until all readers leave.

RWMutex pays off when reads dominate and each read does real work under the lock. For tiny critical sections like a single map lookup, a plain Mutex is often just as fast, because the read lock has its own bookkeeping. Benchmark before choosing.

You cannot upgrade a read lock to a write lock. Calling Lock while holding RLock in the same goroutine deadlocks. Release the read lock first, then take the write lock and check the condition again, since another writer may have changed the data in between.

sync/atomic for single values

For one counter or flag, sync/atomic is simpler and cheaper than a mutex. The typed wrappers (Go 1.19) are the ones to use:

Atomics protect one value at a time. As soon as two values must change together (a balance and a transaction count, a map and its size), use a mutex. Two separate atomic operations can interleave with other goroutines between them.

sync.Once

sync.Once runs a function exactly once, no matter how many goroutines call it at the same time. Everyone who calls Do waits until the first call has finished. It is the standard way to initialize something lazily:

Each "runs once" line appears exactly once. If the function passed to Do panics, Once still counts it as done and never retries. sync.OnceValues does the same for functions that return two values, typically a value and an error.

Mutex, channel, or sync.Map

SituationUse
A struct or map that several goroutines update in placesync.Mutex in the struct
Mostly reads, occasional writes, reads do real worksync.RWMutex
A single counter or flagsync/atomic
One-time initializationsync.Once, sync.OnceValue
Handing data from one goroutine to anothera channel
A cache whose keys are written once and read many times, or goroutines touching disjoint keyssync.Map

sync.Map is not a general replacement for a locked map. It has no type parameters, so values come back as any, and it is only faster in the two cases in the table. Start with a mutex and a normal map.

Common mistakes

  • Copying a mutex. Passing a struct that contains a sync.Mutex by value, or using a value receiver, copies the lock. go vet reports passes lock by value or copies lock value.
  • Locking twice in one goroutine. Go mutexes are not reentrant. If Inc calls Get and both take the lock, Inc blocks forever. Have public methods lock, and private helpers assume the lock is held.
  • Forgetting to unlock on an early return. Use defer unless you have a reason not to.
  • Locking in different orders. If one goroutine takes lock A then B while another takes B then A, both can wait on each other forever. Always acquire multiple locks in the same order.
  • Exposing the guarded data. Returning the internal map from a method lets callers read and write it without the lock. Return a copy (maps.Clone, Go 1.21) or a single value.
  • Protecting only the writes. Unlocked reads concurrent with locked writes are still races. Run your tests with go test -race.

Frequently Asked Questions

What is a mutex in Go?

A sync.Mutex is a lock that lets only one goroutine at a time run the code between mu.Lock() and mu.Unlock(). You use it to protect data that several goroutines read and write, such as a map or a struct. Its zero value is an unlocked mutex, ready to use.

When should I use RWMutex instead of Mutex?

When reads greatly outnumber writes and each read holds the lock for a meaningful amount of time. RLock lets any number of readers in at once, while Lock waits for exclusive access. For short critical sections a plain Mutex is often as fast or faster, so measure before switching.

Is a Go map safe for concurrent use?

No. Concurrent reads are fine, but a write concurrent with any other read or write is a data race, and the runtime usually detects it and crashes with fatal error: concurrent map writes (or concurrent map read and map write). Protect the map with a sync.Mutex or sync.RWMutex, or use sync.Map for its specific use cases.

Is sync.Mutex reentrant in Go?

No. If a goroutine that holds the lock calls Lock again, it blocks forever waiting for itself. Structure the code so that exported methods take the lock and call unexported helpers that assume it is already held.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED