the following code will result in a data race detection when run with go run -race main.go
:
func main() {
var values *map[string]string
go func() {
for {
// First completely initialize the map
newMap := make(map[string]string)
newMap["foo"] = "bar"
// Write to shared pointer variable
values = &newMap
time.Sleep(time.Second)
}
}()
go func() {
for {
// Save pointer variable
savedMap := values
if savedMap == nil {
continue
}
// Do something with the hopefully safe map!
fmt.Println("savedMap:", savedMap)
time.Sleep(time.Second)
}
}()
}
Snippet of output:
==================
WARNING: DATA RACE
Write at 0x00c000120f10 by goroutine 28:
main.main.func1()
My question is: Is it really unsafe? Could I really end up with a mangled pointer address (one part of the new one and one part of the old one)? Or is this just a "best-practice" kind of thing where doing this might result in hard to debug problems later when additional code does not adhere to the "contract"?
In my use case I hardly ever update the pointer. 99.9% of the times this pointer will be read. So I hoped to not having to use synchronization features like atomic.Pointer/Mutexes and so on to safe on complexity and also surely performance (even though maybe minimally).
It's not (only) about how you're accessing the pointer. The pointer read is "relatively safe", because the Go memory model promises that values of machine word size or smaller can't have "torn reads" or read back a value that was never written. But there's nothing that guarantees that a different goroutine will see the result of the assignment to values
only after the initialization of newMap
! Guarantees are only provided in terms of "synchronizing operations" such as channel writes, mutex locks, or sync/atomic
accesses, by which all writes that "happen before" a synchronizing event in one goroutine are guaranteed to be visible to a synchronized-with goroutine.
The Go memory model doc specifically calls out a very similar example, in which one goroutine runs
func setup() {
a = "hello, world"
done = true
}
and another waits for done
to be true and then prints a
, however the doc notes
but there is no guarantee that, in
doprint
, observing the write todone
implies observing the write toa
. This version can (incorrectly) print an empty string instead of "hello, world".
Similarly, in your code, there is no guarantee that observing the write to values
implies observing the complete initialization of newMap
. Therefore the Println
could print an incorrect value, print an empty map, or simply crash.