Understanding Go Map and Slice Concurrency Safety and Parameter Passing
The article explains that Go maps and slices are not safe for concurrent reads or writes, describes how the runtime detects map violations, and recommends using sync.Mutex, sync.RWMutex, or sync.Map for maps and external synchronization for slices, while also clarifying that passing these reference types to functions shares underlying data unless explicitly copied.
Because cloud migration trends push business code toward the Go technology stack, developers often encounter issues related to Go's concurrency safety and parameter passing semantics. This article summarizes the pitfalls and solutions when using maps and slices concurrently, and when passing them as function arguments.
1. Map concurrent read/write triggers a fatal error
Classic example that causes a runtime panic:
var testMap = map[string]string{}
func main() {
go func() {
for {
_ = testMap["bar"]
}
}()
go func() {
for {
testMap["bar"] = "foo"
}
}()
select {}
}The program aborts with fatal error: concurrent map read and map write because Go's built‑in map type is not safe for concurrent access. The runtime detects the unsafe operation and aborts to prevent data corruption.
2. How Go detects map concurrency violations
In the Go source file map.go the following flag bits are defined:
// flags
iterator = 1 // there may be an iterator using buckets
oldIterator = 2 // there may be an iterator using oldbuckets
hashWriting = 4 // a goroutine is writing to the map
sameSizeGrow = 8 // the current map growth is to a new map of the same sizeRead operations check the hashWriting flag before accessing the map:
if h.flags & hashWriting != 0 {
throw("concurrent map read and map write")
}Write operations perform a two‑step check: they first verify the flag, set it, perform the write, then clear the flag.
// pre‑write check
if h.flags & hashWriting != 0 {
throw("concurrent map writes")
}
// set flag
h.flags ^= hashWriting
// ... actual write ...
// post‑write check
if h.flags & hashWriting == 0 {
throw("concurrent map writes")
}
// clear flag
h.flags &^= hashWriting3. Avoiding map concurrency problems
Because making the map itself concurrent‑safe would add a large performance overhead, Go encourages developers to use external synchronization or the specialized sync.Map when read‑heavy/write‑light patterns are expected.
Manual lock‑based solution:
type concurrentMap struct {
sync.RWMutex
m map[string]string
}
func main() {
var testMap = &concurrentMap{m: make(map[string]string)}
// write
testMap.Lock()
testMap.m["a"] = "foo"
testMap.Unlock()
// read
testMap.RLock()
fmt.Println(testMap.m["a"])
testMap.RUnlock()
}Note that heavy contention on the lock can degrade performance.
Using sync.Map (suitable for many reads, few writes):
var m sync.Map
// write
m.Store("test", 1)
m.Store(1, true)
// read
val1, _ := m.Load("test")
val2, _ := m.Load(1)
fmt.Println(val1.(int))
fmt.Println(val2.(bool))
// iterate
m.Range(func(key, value interface{}) bool {
// ...
return true
})
// delete
m.Delete("test")
// load or store
m.LoadOrStore("test", 1)Key characteristics of sync.Map :
Read‑only part ( read ) is accessed atomically, allowing lock‑free reads.
Writes go to a separate dirty map protected by a mutex.
When the read miss count reaches the size of dirty , the dirty map is promoted to the read map.
Best used when reads dominate writes.
4. Slice concurrency safety
Like maps, slices are not safe for concurrent reads and writes. The following program demonstrates data races that do not cause a panic but can produce corrupted results:
var testSlice []int
func main() {
for i := 0; i < 1000; i++ {
go func() {
testSlice = append(testSlice, i)
}()
}
for idx, val := range testSlice {
fmt.Printf("idx:%d val:%d\n", idx, val)
}
}Because the slice header (pointer, length, capacity) is shared, concurrent appends race with each other.
5. Slice as a function parameter
Go passes arguments by value, but for reference types such as slices and maps the value is a shallow copy that still points to the same underlying data. Therefore modifications inside a function affect the original slice:
func changeVal(testSlice []string, idx int, val string) {
testSlice[idx] = val
}
func main() {
var testSlice []string
testSlice = make([]string, 5)
testSlice[0] = "foo"
changeVal(testSlice, 0, "bar")
fmt.Println(testSlice[0]) // prints "bar"
}If the slice grows beyond its capacity, a new backing array is allocated and the caller's slice no longer sees the changes:
func appendVal(testSlice []string, val string) {
fmt.Printf("testSlice:%p\n", testSlice)
testSlice = append(testSlice, "addCap") // triggers reallocation
fmt.Printf("after append testSlice:%p\n", testSlice)
testSlice[0] = val
}
func main() {
var testSlice []string
testSlice = make([]string, 5)
testSlice[0] = "foo"
appendVal(testSlice, "bar")
fmt.Println(testSlice[0]) // still "foo"
}To obtain an independent copy you can allocate a new slice and use the built‑in copy function:
var newTestSlice []string
newTestSlice = make([]string, len(testSlice))
copy(newTestSlice, testSlice)
fmt.Printf("testSlice:%p\n", testSlice)
fmt.Printf("newTestSlice:%p\n", newTestSlice)Arrays converted to slices suffer the same issue; be cautious when passing them to functions.
6. Map as a function parameter
Because a map is a reference type, the function receives a shallow copy of the map header, and any write operation directly modifies the underlying map:
func changeMap(testMap map[string]string, k, v string) {
testMap[k] = v
}
func main() {
var testMap map[string]string
testMap = make(map[string]string)
testMap["foo"] = "bar"
changeMap(testMap, "foo", "rab")
fmt.Println(testMap) // map[foo:rab]
}7. Summary
Go’s simplicity and performance make it a popular choice for micro‑service development, but its language semantics introduce subtle pitfalls:
Maps and slices are not safe for concurrent read/write; use explicit locks or sync.Map for maps.
When passing slices or maps to functions, remember they are shallow copies; modifications affect the original data unless a deep copy is made.
Slice reallocation breaks the link between caller and callee, which can be used deliberately to avoid unintended side effects.
Understanding the runtime flags and the internal design of sync.Map helps you choose the right concurrency primitive for your workload.
By being aware of these behaviors, developers can write correct and efficient Go code in high‑concurrency scenarios.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.