The usual way to add a golang mutex is sync.Mutex from the sync package: call Lock before touching shared state and Unlock after. That go mutex pattern gives you a golang mutex lock so only one goroutine runs the critical section at a time, which avoids data races when multiple goroutines update the same memory.
Mutex in golang code is still explicit—you choose which variables are protected and how big the critical sections are. This guide shows a golang mutex example with the race detector, the fixed Lock/Unlock version, when sync.RWMutex helps, and how to keep the lock beside the data it guards.
Tested with Go 1.24 on Linux (
go runandgo run -race).
Why shared memory needs a lock
When two goroutines update the same variable without coordination, the scheduler can interleave their instructions. The result is a data race: the program can print wrong totals, corrupt structures, or crash. The race detector (go run -race) instruments the binary and reports conflicting accesses at runtime.
Below, many goroutines increment one int. The final value is unpredictable without a mutex, and the race detector reports the problem.
package main
import (
"fmt"
"sync"
)
var n int
func main() {
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
n++
}
}()
}
wg.Wait()
fmt.Println("n =", n)
}==================
WARNING: DATA RACE
Read at 0x0000005ff650 by goroutine 7:
main.main.func1()
/tmp/race_demo.go:17 +0x91
...
==================
n = 20000
Found 2 data race(s)
exit status 66Your line numbers and goroutine IDs will differ; what matters is WARNING: DATA RACE and Found N data race(s) with a non-zero exit status. Run the same snippet locally with go run -race yourfile.go to reproduce the report.
golang mutex lock and unlock (sync.Mutex)
The usual golang lock pattern is: acquire the mutex, do the smallest amount of work that touches shared state, then release it. Lock blocks until the mutex is free; Unlock must run on the same goroutine that called Lock for sync.Mutex.
Wrapping the increment removes the race: only one goroutine mutates n at a time.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var n int
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
n++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("n =", n)
}After Run (or go run), you should see n = 5. With go run -race, this version completes with exit code 0 and no race warnings.
A common refinement is defer mu.Unlock() immediately after mu.Lock() so early returns still release the lock:
mu.Lock()
defer mu.Unlock()
// critical section onlysync.RWMutex (many readers, one writer)
sync.RWMutex is still a mutex in golang, but it distinguishes read locks from write locks. Many goroutines can hold RLock at once for read-only critical sections; writers use Lock/Unlock, which exclude everyone else. Method names are case-sensitive: RLock and RUnlock, not Rlock.
Use RWMutex when reads dominate writes and read sections are long enough that allowing concurrent reads helps. If updates are frequent or sections are tiny, a plain sync.Mutex is often simpler and fast enough.
package main
import (
"fmt"
"sync"
)
type Wallet struct {
mu sync.RWMutex
cash int
}
func (w *Wallet) Balance() int {
w.mu.RLock()
defer w.mu.RUnlock()
return w.cash
}
func (w *Wallet) Deposit(amount int) {
w.mu.Lock()
defer w.mu.Unlock()
w.cash += amount
}
func main() {
w := &Wallet{cash: 100}
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("balance", w.Balance())
}()
}
wg.Add(1)
go func() {
defer wg.Done()
w.Deposit(50)
}()
wg.Wait()
fmt.Println("final", w.Balance())
}Run it locally; output order varies, but readers only call RLock/RUnlock while Deposit uses exclusive Lock/Unlock, so cash is never read mid-write.
Mutex with structs: named field or embedding
The Wallet example keeps a named mu field next to cash. That makes it obvious which golang lock guards which data and avoids promoted methods on the struct.
You can instead embed sync.Mutex (or sync.RWMutex) in the struct so callers use promoted Lock/Unlock. That is valid, but you must not copy the struct by value once goroutines use it; copies duplicate the mutex in an invalid state. Prefer sharing a single *T with mutex methods on pointer receivers. The sync.Mutex documentation states that a mutex must not be copied after first use.
Summary
Golang mutex usage centers on sync.Mutex with Lock and Unlock (or defer Unlock) around shared updates. go run -race catches mistakes early. sync.RWMutex adds RLock/RUnlock for concurrent readers and Lock/Unlock for writers when read-heavy workloads benefit. Place the lock with the data it protects—typically on a struct accessed through pointers—and never copy a Mutex or RWMutex after use.

