Golang mutex: sync.Mutex lock and unlock with examples

Mutex in golang with sync.Mutex: Lock and Unlock, fix data races with go run -race, golang mutex lock patterns, defer unlock, sync.RWMutex for many readers, embed a mutex in a struct.

Published

Updated

Read time 4 min read

Reviewed byDeepak Prasad

Golang mutex: sync.Mutex lock and unlock with examples

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 run and go 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.

go
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)
}
text
==================
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 66

Your 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.

go
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)
}
Output

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:

go
mu.Lock()
defer mu.Unlock()
// critical section only

sync.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.

go
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.


References


Frequently Asked Questions

1. What does a golang mutex do?

A mutex (mutual exclusion lock) lets one goroutine at a time run a critical section; others block until Unlock. In Go use sync.Mutex with Lock and Unlock.

2. What is the difference between sync.Mutex and sync.RWMutex?

sync.Mutex is exclusive for every critical section. sync.RWMutex allows many concurrent readers with RLock/RUnlock while writers use Lock/Unlock.

3. Should I always defer mu.Unlock() after mu.Lock()?

Deferring Unlock right after Lock is the usual pattern so every return path releases the lock; keep the critical section short and avoid calling blocking code while holding the lock.

4. Can I copy a sync.Mutex?

Do not copy a Mutex or RWMutex; pass a pointer if structs are copied, or embed the mutex in a struct you only use by pointer.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.