1. Mutexes in Go

Mutex is short for mutual exclusionarrow-up-right, and the conventional name for the data structure that provides it is "mutex", often abbreviated to "mu".

It's called "mutual exclusion" because a mutex excludes different threads (or goroutines) from accessing the same data at the same time.

Mutexes allow us to lock access to data. This ensures that we can control which goroutines can access certain data at which time.

Go's standard library provides a built-in implementation of a mutex with the sync.Mutexarrow-up-right type and its two methods:

We can protect a block of code by surrounding it with a call to Lock and Unlock as shown on the protected() function below.

It's good practice to structure the protected code within a function so that defer can be used to ensure that we never forget to unlock the mutex.

func protected(){
    mu.Lock()
    defer mu.Unlock()
    // the rest of the function is protected
    // any other calls to `mu.Lock()` will block
}

Mutexes are powerful. Like most powerful things, they can also cause many bugs if used carelessly.

Maps are not safe for concurrent use! If you have multiple goroutines accessing the same map, and at least one of them is writing to the map, you must lockarrow-up-right your maps with a mutex.

Assignment

We send emails across many different goroutines at Textio. To keep track of how many we've sent to a given email address, we use an in-memory map.

Our safeCounter struct is unsafe! Update the inc() and val() methods so that they utilize the safeCounter's mutex to ensure that the map is not accessed by multiple goroutines at the same time.

circle-info

Note: Wasm Is Single-Threaded

The execution engine used here runs Go in WebAssembly, which is single-threaded, so maps are thread-safe in this environment. The exercises simulate a multi-threaded environment with slowIncrement and slowVal functions. In real-world Go, code may run on multi-core machines, so you should always write code that is safe for concurrent access.

What is a Mutex? (Real-World Analogy)

Imagine a shared bathroom in an office:

1

Person A enters and locks the door (mu.Lock()).

2

Person B tries to enter but has to wait (blocked).

3

Person A finishes and unlocks (mu.Unlock()).

4

Person B can now enter.

The mutex ensures only ONE person uses the bathroom at a time.

Why Do We Need Mutexes in Code?

When multiple goroutines (think: multiple workers) access the same data simultaneously:

Problem without mutex:

Both read 5, both add 1, both write 6 β†’ we lost a count!

Solution with mutex:

Only one goroutine can access the data at a time.

Your Assignment Solution

You need to protect the counts map in safeCounter using the mutex:

Key Concepts Explained

Why defer mu.Unlock()?

defer ensures the unlock happens even if the function panics or returns early:

Why protect val() even though it only reads?

Even reads need protection! Consider:

Rule: If ANY goroutine writes, ALL access (read + write) must be protected.

What happens when a goroutine tries to Lock() while another holds it?

It blocks (waits) until the lock is released:

Common Mistakes

Mistake 1: Forgetting to unlock

Mistake 2: Locking twice in same goroutine

Mistake 3: Accessing data without lock

Mental Model

Think of mutex as a token:

  • Only one goroutine can hold the token at a time

  • Lock() = "grab the token (wait if someone else has it)"

  • Unlock() = "give back the token (let others use it)"

  • The protected code = "what you do while holding the token"

Complete Solution for Your Assignment

That's it! The mutex ensures that only one goroutine can access counts at a time, preventing race conditions.

Does this make sense now? The key is: Lock β†’ Access Data β†’ Unlock, and defer makes sure you never forget to unlock!