Vincent Blanchon
Jul 1 · 4 min read
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article follows “Go: Map Design by Code” that describes the internal implementation of the map.

The article dedicated to the maps in the Go blog is clear that:

Maps are not safe for concurrent use: it’s not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism

However, as explained in the FAQ, Google provides some help:

As an aid to correct map use, some implementations of the language contain a special check that automatically reports at run time when a map is modified unsafely by concurrent execution.

Data race detection

The first help we can get from Go is the data race detection. Running your program or your test with the flag -race will give you an idea of potential data race. Let’s look at an example:

func main() {
m := make(map[string]int, 1)
m[`foo`] = 1

var wg sync.WaitGroup

wg.Add(2)
go func() {
for i := 0; i < 1000; i++ {
m[`foo`]++
}
}()
go func() {
for i := 0; i < 1000; i++ {
m[`foo`]++
}
}()
wg.Wait()
}

In this example, we clearly see that at a point of time, the two goroutines will try to write a new value simultaneously. Here is the output of the race detector:

==================
WARNING: DATA RACE
Read at 0x00c00008e000 by goroutine 6:
runtime.mapaccess1_faststr()
/usr/local/go/src/runtime/map_faststr.go:12 +0x0
main.main.func2()
main.go:19 +0x69

Previous write at 0x00c00008e000 by goroutine 5:
runtime.mapassign_faststr()
/usr/local/go/src/runtime/map_faststr.go:202 +0x0
main.main.func1()
main.go:14 +0xb8

The race detector explains that the second goroutine is reading while the first goroutine is writing a new value at this memory location. If you would learn more about that, I suggest you read my article about the data race detector.

Concurrent writing detection

Another aid provided by Go is the concurrency writing detection. Let’s use the same example we saw previously. Running this program will show us an error:

fatal error: concurrent map writes

Go manages this concurrency thanks to internal flag flags hold in the map structure. Then, when the code tried to modify the map (assign a new value, delete a value or clear the map), one of the bits of the flags is set to 1:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
h.flags ^= hashWriting

The value of hashWriting is 4 and will set the corresponding bit to 1.

Operation ^ is a XOR operation that set a bit to 1 if the bits of two operands are opposite:

Then, this flag will reset at the end of the operation:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
h.flags &^= hashWriting
}

Now that a control is set for each operation that modifies the map, it can prevent concurrent writing by checking the status of this flag. Here is an example of the flag life cycle:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
[...]
// if another process is currently writing, throw error
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
[...]
// no one is writing, we can set now the flag
h.flags ^= hashWriting
[...] // flag reset
h.flags &^= hashWriting
}

sync.Map vs Map with lock

The sync package also provides a Map that is safe for concurrent usage. However, as explained in the documentation, it is preferable to choose carefully which kind of map you should use:

The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content.

Indeed, as explained in my article “Map Design by Code”, the map provides functions according to the type of map we are dealing with.

Let’s run a simple benchmark between a regular map with a lock and a map from the sync package. One benchmark will concurrently write values in the maps while the second one will only read values in the maps:

MapWithLockWithWriteOnlyInConcurrentEnc-8  68.2µs ± 2%
SyncMapWithWriteOnlyInConcurrentEnc-8 192µs ± 2%
MapWithLockWithReadOnlyInConcurrentEnc-8 76.8µs ± 3%
SyncMapWithReadOnlyInConcurrentEnc-8 55.7µs ± 4%

As we can see, neither map is better than the other . Depending on the situation we could choose either one of them. Those situation are well explained in the documentation:

The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.

Map vs sync.Map

The FAQ also explained why they made the choice to not make the map concurrency safe by default:

Therefore requiring that all map operations grab a mutex would slow down most programs and add safety to few

Let’s run a benchmark that will not work with concurrent goroutines in order to understand the possible impact of a map safe for concurrency shipped by default in the library if you do not need it:

MapWithWriteOnly-8          11.1ns ± 3%
SyncMapWithWriteOnly-8 121ns ± 6%
MapWithReadOnly-8 4.87ns ± 7%
SyncMapWithReadOnly-8 29.2ns ± 4%

The simple map is 7 to 10 times faster. This obviously sounds logical in a non-concurrency mode, but the huge difference definitely explains why it is better to not have the map natively concurrently safe. If you do not need to deal with concurrency, why make the programs slower?

A Journey With Go

A Journey With Go Language Programming

Vincent Blanchon

Written by

French Gopher in Dubai

A Journey With Go

A Journey With Go Language Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade