skip to content

What is Go's sync.Map, and why can't several goroutines share a plain map instead?

level: juniorimportance: must knowfreq 55%

answer

  1. plain maps carry no lock of their own
  2. zero value, no make needed
  3. keys and values are any, not typed
  4. safe methods: Load, Store, Range
  5. specialised container, not a faster map

basics

~20 s

sync.Map is a map in Go's sync package whose methods — Load, Store, LoadOrStore, Range — are safe to call from many goroutines with no lock of your own. A plain Go map is not: concurrent access with a writer is a data race.

solid answer

~50 s

A plain Go `map` has no internal synchronisation, so if one goroutine writes while any other reads or writes, the program has a data race and its behaviour is undefined. `sync.Map` is the standard library's own concurrent map: its zero value is ready to use (no `make`), and `Load`, `Store`, `LoadOrStore`, `LoadAndDelete`, `Delete` and `Range` may all be called from any number of goroutines. The price is that keys and values are `any`, so you box on the way in and type-assert on the way out, and there is no `Len`. It also must not be copied after first use — pass a `*sync.Map` or keep it as a field accessed through a pointer. Importantly it is not the default answer to "I need a shared map": a plain typed map guarded by a lock is usually the better choice, and `sync.Map` is documented for two specific access patterns.

code

go · 17 lines
go
type connMeta struct {
	user string
}

var conns sync.Map // conn id (string) -> *connMeta

func register(id, user string) {
	conns.Store(id, &connMeta{user: user})
}

func userFor(id string) (string, bool) {
	v, ok := conns.Load(id)
	if !ok {
		return "", false
	}
	return v.(*connMeta).user, true // Load returns any
}

go deeper

for a junior

Be ready to say plainly that a Go map has no lock inside it, that concurrent use with a writer is a data race, and that sync.Map's zero value is ready to use with Load, Store and Range.

for a middle

Explain the mechanics you pay for: keys and values are any, so reads need a type assertion and non-pointer values box into an interface. Explain why the map must never be copied after first use.

for a senior

Show the judgment: a typed map guarded by a lock is the default, and sync.Map is reached for only when the access pattern matches the two it is documented for. Be able to say what you would measure before switching.

for a principal

Own the API consequence: a package that exposes a sync.Map on its surface exports an untyped, uncountable container that callers cannot extend. Prefer a typed wrapper whose internals you can replace without breaking anyone.

## The problem it solves A Go `map` is an ordinary in-memory data structure with no locking of any kind. That is a deliberate design decision: most maps are used by one goroutine, and paying for synchronisation on every lookup would be a tax on the common case. The consequence is that the moment two goroutines touch the same map and at least one of them writes, the program contains a **data race**. A racing program has no defined behaviour at all — you cannot reason about it, and "it worked in testing" proves nothing, because the race detector (`go test -race`, `go build -race`) only reports races on code paths that actually executed during the run. A read-only map is different: once a map is fully built and then never written again, any number of goroutines may read it concurrently. The rule is about *concurrent writes*, not about maps being magic. ## What sync.Map is `sync.Map` is a concurrent map provided by the standard library's `sync` package. Its whole API is method calls on the map value: - `Store(key, value any)` — set a key. - `Load(key any) (value any, ok bool)` — read a key; `ok` reports presence, exactly like the two-value form of a plain map read. - `LoadOrStore(key, value any) (actual any, loaded bool)` — get the existing entry, or install this one, in a single step. - `LoadAndDelete(key any) (value any, loaded bool)` — remove a key and hand its value back to exactly one caller. - `Delete(key any)` — remove a key, telling you nothing. - `Range(f func(key, value any) bool)` — walk the entries; returning `false` from `f` stops the walk. - `Swap`, `CompareAndSwap`, `CompareAndDelete` and `Clear` round out the set. All of these are safe for concurrent use by multiple goroutines. You never take a lock yourself. ## The three things beginners get wrong **It needs no initialisation.** The zero value is an empty, usable map. `var m sync.Map` is complete; there is no `make` and no constructor. This mirrors `sync.Mutex`, whose zero value is an unlocked mutex. **It must not be copied after first use.** Because it carries internal state, copying a `sync.Map` value — passing it by value to a function, or copying a struct that embeds one — produces two half-broken maps. Keep it behind a pointer: a method on `*Registry` where `Registry` has a `conns sync.Map` field is fine; a method on `Registry` (value receiver) is not. **It is not typed.** Keys and values are `any`. Storing a `*connMeta` and reading it back gives you an `any` you must assert: `v.(*connMeta)`. There is no compile-time protection against storing the wrong type into the wrong map, and boxing a non-pointer value into an interface generally costs an allocation. The usual remedy is to wrap the `sync.Map` in a small unexported struct that exposes typed `Get`/`Put` methods, so exactly one file does the assertions. ## A concrete shape A server that tracks live client connections wants a registry keyed by connection id, holding metadata about each connection — who authenticated, when the connection opened. Connections are added when a client arrives, removed when it leaves, and read constantly by unrelated request handlers that need to look up "is this connection still around, and who owns it?". Every one of those touches happens on a different goroutine, so an unguarded plain map is out from the first line of code. ```go var conns sync.Map // conn id (string) -> *connMeta conns.Store("c-42", &connMeta{user: "ada"}) if v, ok := conns.Load("c-42"); ok { fmt.Println(v.(*connMeta).user) } ``` ## When it is *not* the answer The honest default for shared mutable state in Go is a plain typed map with a lock beside it, in a struct, with methods that take the lock. It keeps types, keeps `len`, allocates less, and is obvious to every reader. `sync.Map`'s own documentation limits its claim to two access patterns: entries written once and then read many times (a cache that only grows), and several goroutines working on **disjoint** sets of keys. Outside those, a lock-guarded map commonly measures faster, and "I used sync.Map because it is the concurrent one" is exactly the reasoning an interviewer is probing for. The useful mental model: `sync.Map` is a specialised container with an awkward API and a real cost, not a free upgrade over `map`.

  • Does a sync.Map need initialising, and may you pass one by value?
    The zero value is an empty, usable map — `var m sync.Map` is complete, there is no `make` and no constructor. But it must not be copied after first use, so never pass it by value or copy a struct containing one; hold it through a pointer, and give any wrapper struct pointer receivers.
  • What type do sync.Map's Load and Range hand back, and what does that cost you?
    Both give you `any`. You type-assert on every read, you get no compile-time checking that the right type went in, and storing a non-pointer value boxes it into an interface, which generally allocates. The usual fix is a small typed wrapper struct so the assertions live in one place.
  • Which methods does sync.Map offer beyond Load and Store?
    `LoadOrStore` and `LoadAndDelete` for atomic get-or-create and remove-and-take, `Delete`, `Range` for iteration, `Swap`, `CompareAndSwap` and `CompareAndDelete` for conditional updates, and `Clear` to empty it. There is deliberately no `Len`.

A plain map is a notebook on a shared desk: fine while one person writes in it, corrupted the moment two do. sync.Map is a notebook with a built-in turnstile — everyone may use it, but pages come out unlabelled and you must work out what each one is.

saying these in an interview costs you the question

  • Calls sync.Map simply a faster map for every workload
  • Says a plain map is safe when only one goroutine writes
  • Thinks sync.Map must be created with make before use
  • Expects Load to return the value's real type without an assertion
  • Passes a sync.Map by value into a helper function
  • Assumes Go maps take an internal lock automatically