Skip to Content
Course content

161: sync.Map for Concurrent Maps

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

I once worked with a developer who spent three straight days chasing a phantom crash that only happened in production. He had built a high-throughput API gateway and used a standard Go map to cache authentication tokens. To make it "thread-safe," he wrapped the map in a sync.RWMutex. On paper, it was perfect. But under heavy load, the service started panicking with fatal error: concurrent map read and map write. It turned out that in one obscure edge case—a token refresh logic—he had forgotten to acquire the lock before a write. Because Go maps are notoriously unforgiving about concurrent access, the whole process died instantly. He didn't need a bigger lock; he needed a different tool.

When the Mutex Becomes the Bottleneck

Now, don't get me wrong: a map with a sync.RWMutex is the right choice for 90% of your Go projects. It's explicit, it's fast, and you have total control over the types. But there's a specific pattern where that mutex actually starts slowing you down. When you have a map that is read from thousands of times per second, but only updated occasionally, the lock contention on that RWMutex can become a performance killer.

This is exactly why the Go team gave us sync.Map. It's a specialized concurrent map implementation that optimizes for two specific cases: when the entry for a given key is only ever written once but read many times, or when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. It does this by using a clever combination of a read-only map and a "dirty" map to minimize lock contention.

Navigating the sync.Map API

The first thing you'll notice is that sync.Map doesn't use generics (it's an older part of the library). Everything goes in as an any and comes out as an any. This means you'll be doing a fair bit of type assertion. I'll be honest: it's a bit clunky compared to modern Go, but it's the price you pay for the internal lock-free optimizations.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var cache sync.Map

	// Store a value
	cache.Store("user_123", "Session_A")

	// Load a value (requires type assertion)
	if val, ok := cache.Load("user_123"); ok {
		fmt.Println("Found session:", val.(string))
	}

	// LoadOrStore is the "magic" method. 
	// It loads the value if it exists, otherwise stores the provided value.
	// This is critical for avoiding race conditions during cache initialization.
	actual, loaded := cache.LoadOrStore("user_456", "Session_B")
	fmt.Printf("Actual: %v, Loaded: %v\n", actual, loaded)

	// Range iterates over the map. 
	// You return 'true' to continue, 'false' to stop.
	cache.Range(func(key, value any) bool {
		fmt.Printf("Key: %v, Value: %v\n", key, value)
		return true
	})
}

Choosing the Right Map for the Job

I see developers reach for sync.Map the moment they see the word "concurrent," but that's a mistake. Because it uses any and internal overhead to manage its two-map system, it can actually be slower than a standard map with a mutex if your workload involves constant writes to the same keys.

  • Use map + sync.RWMutex if you have a high volume of writes, or if you need the type safety of generics to avoid the overhead and risk of type assertions.
  • Use sync.Map if you have a "read-heavy" cache where keys are stable, or if different goroutines are operating on entirely different keys.

One last tip: if you find yourself using sync.Map and you're constantly casting types in every single function, consider wrapping the sync.Map in your own struct. Provide typed methods (like GetSession(id string) string) to hide the any casting from the rest of your business logic. Your future self will thank you.




📋 Practical Task

Implementing a Thread-Safe API Token Cache

You are building a middleware component that caches API tokens to avoid hitting a database on every request. Currently, the implementation uses a standard map and is crashing under load due to concurrent access.

Your Goal: Refactor the following code to use sync.Map. You must ensure that the GetToken method handles the type assertion correctly and that the SetToken method stores the value safely.

package main

import (
	"fmt"
	"sync"
)

type TokenCache struct {
	// TODO: Replace the map and mutex with a sync.Map
	mu    sync.RWMutex
	tokens map[string]string
}

func (c *TokenCache) SetToken(userID string, token string) {
	// TODO: Implement using sync.Map Store
}

func (c *TokenCache) GetToken(userID string) (string, bool) {
	// TODO: Implement using sync.Map Load with proper type assertion
	return "", false
}

func main() {
	cache := &TokenCache{
		// Initialize your sync.Map here if necessary
	}

	var wg sync.WaitGroup
	// Simulate 100 concurrent writers and readers
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			user := fmt.Sprintf("user_%d", id%10)
			token := fmt.Sprintf("token_%d", id)
			cache.SetToken(user, token)
			cache.GetToken(user)
		}(i)
	}
	wg.Wait()
	fmt.Println("Successfully processed concurrent requests.")
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.