← Back to Go Course | Chapter 11: Concurrency | Lesson 6 of 8

sync.Mutex

A mutex is like a single bathroom key -- only one goroutine can hold it and use the shared resource at a time, and everyone else has to wait their turn.

Protecting Shared State

A sync.Mutex ensures that only one goroutine at a time can execute the code between Lock() and Unlock(), preventing concurrent goroutines from corrupting shared data by modifying it simultaneously.

Example: Protecting Shared State

markup
package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	mu    sync.Mutex
	value int
}

func (c *Counter) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

func main() {
	c := &Counter{}
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Increment()
		}()
	}
	wg.Wait()
	fmt.Println(c.value)
}

Always Pair Lock with defer Unlock

Using defer immediately after Lock() guarantees Unlock() runs no matter how the function exits -- including early returns or a panic -- which avoids permanently locking out every other goroutine.

Example: Always Pair Lock with defer Unlock

markup
package main

import (
	"fmt"
	"sync"
)

type SafeMap struct {
	mu   sync.Mutex
	data map[string]int
}

func (m *SafeMap) Set(key string, value int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.data[key] = value
}

func main() {
	m := &SafeMap{data: make(map[string]int)}
	m.Set("a", 1)
	fmt.Println(m.data)
}

What Happens Without a Mutex

Without a mutex (or other synchronization), multiple goroutines reading and writing the same variable concurrently produce a data race -- an unpredictable, sometimes silently wrong result that can differ from run to run.

Note: Run the race detector ('go run -race') on real concurrent code to catch missing synchronization.

Example: What Happens Without a Mutex

markup
package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	total := 0
	var wg sync.WaitGroup
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			total++
			mu.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println("total:", total)
}
Common Mistakes
  1. Forgetting to call Unlock() after Lock(), permanently blocking every other goroutine waiting for that mutex.
  2. Locking a mutex, then returning early on an error path without unlocking -- always pair Lock() with a deferred Unlock().
  3. Copying a struct containing a sync.Mutex by value, which duplicates the lock and breaks the mutual exclusion guarantee.
Chapter Summary
  • sync.Mutex provides mutual exclusion: only one goroutine can hold the lock at a time.
  • Lock() acquires the mutex (blocking if another goroutine holds it), Unlock() releases it.
  • defer mu.Unlock() right after Lock() is the standard way to guarantee release, even on early returns.
  • A mutex protects shared data from concurrent, unsynchronized access that would otherwise cause a data race.
🔒

Chapter Quiz — Complete all 8 topics to unlock

0/8 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.