Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this a lot in code reviews: a developer swaps out every single sync.Mutex for a sync.RWMutex, thinking they've just given the application a free performance boost. The logic seems sound—if multiple goroutines can read the data at once, the program should run faster, right?

The "RWMutex is always faster" Fallacy

Here is the cold truth: RWMutex is more complex internally than a standard Mutex. A simple Mutex is essentially a binary flag. An RWMutex, however, has to keep track of how many readers are currently holding the lock, handle writer queues, and manage the hand-off between readers and writers. This bookkeeping isn't free; it costs CPU cycles.

If your "read" operation is something trivial—like returning a single integer from a struct—the overhead of updating the reader count in an RWMutex can actually take longer than just waiting a few nanoseconds for a standard Mutex to unlock. I once profiled a service where switching back to a plain Mutex for a simple counter actually reduced latency by about 5%.

// This looks "optimized," but if GetValue is called 
// millions of times per second and does almost nothing, 
// the RWMutex overhead might actually slow you down.
type Config struct {
    sync.RWMutex
    value int
}

func (c *Config) GetValue() int {
    c.RLock()
    defer c.RUnlock()
    return c.value
}

Matching the Lock to Your Access Pattern

So, when should you actually reach for the RWMutex? The answer is read-heavy contention. You want an RWMutex when you have a high ratio of reads to writes AND the read operation takes a non-trivial amount of time (like iterating over a map or performing a complex calculation).

Imagine you're building a shared cache of API tokens. You update the token once every hour (a write), but you read it for every single outgoing request (thousands of reads per second). In this scenario, if you used a standard Mutex, every single request would have to wait in a single-file line just to read a string. That's where RLock() shines. It allows a crowd of readers to enter the critical section simultaneously, as long as no one is trying to change the data.

Here is the pattern I follow:

  • Use sync.Mutex by default. It's simpler, faster for low-contention or fast operations, and harder to mess up.
  • Switch to sync.RWMutex only when you've identified a bottleneck where many goroutines are blocking each other just to read the same data.

One last warning: be careful with RLock. If you call RLock() and then, inside that same goroutine, try to call Lock() to update the value, you'll hit a deadlock. You cannot "upgrade" a read lock to a write lock in Go. You have to RUnlock() first, then Lock().




📋 Practical Task

Refactoring the Global Feature Flag Registry

You have been handed a FeatureRegistry struct used by a high-traffic web server. Currently, it uses a sync.Mutex. The registry is read on every single incoming HTTP request to check if a feature is enabled, but it is only updated once every few hours via an admin panel.

The current implementation is causing a bottleneck because thousands of request goroutines are blocking each other just to read the flags map.

Your Task:

  1. Change the sync.Mutex to a sync.RWMutex.
  2. Update the IsEnabled method to use the appropriate "read" locking mechanism so that multiple requests can check flags concurrently.
  3. Ensure the SetFlag method continues to use the exclusive lock to prevent data races during updates.
package main

import (
    "fmt"
    "sync"
)

type FeatureRegistry struct {
    mu    sync.Mutex // CHANGE THIS
    flags map[string]bool
}

func (r *FeatureRegistry) IsEnabled(feature string) bool {
    r.mu.Lock() // OPTIMIZE THIS
    defer r.mu.Unlock()
    return r.flags[feature]
}

func (r *FeatureRegistry) SetFlag(feature string, enabled bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.flags[feature] = enabled
}

func main() {
    reg := &FeatureRegistry{
        flags: make(map[string]bool),
    }
    
    reg.SetFlag("beta-ui", true)
    fmt.Printf("Beta UI enabled: %v\n", reg.IsEnabled("beta-ui"))
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.