We can’t read and write at the same time, this is because writes to a map can cause the map to change. This has to do with how hash maps are implemented, there are a number of great articles written about this, here is a link to the actual source code.
So, if we want to write to a map, we need to always protect it with a mutex or some other mechanism to serialize map access. However, a trick we can use it this specific case is that the map doesn’t change on reads. Therefor if we never directly change the contents of the map, we can read from as many goroutines as we want.
What we can do is to store pointers in the map, after creation we will never change these pointers, just the values they point to. This is technically less efficient since we now how to store a pointer and the actual value, but does solve the race condition. I modified your code to use this principle which looks like this:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// This function allows us to create pointers from literals
func intPtr(newInt int64) *int64 {
return &newInt
}
func main() {
var wg sync.WaitGroup
mapX := map[int]*int64{0: intPtr(1), 1: intPtr(2)}
go func() {
for {
// atomic version of mapX[0]++
atomic.AddInt64(mapX[0], 1)
}
}()
go func() {
for {
// atomic version of mapX[1]++
atomic.AddInt64(mapX[1], 1)
}
}()
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
for key, value := range mapX {
// we need to use atomic.LoadInt64 to synchronize with the atomic.AddInt64 calls
fmt.Printf("%d = %d\n", key, atomic.LoadInt64(value))
}
}()
}
wg.Wait()
}
I hope this answers your question.