Backend Development 12 min read

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.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Go Map and Slice Concurrency Safety and Parameter Passing

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 size

Read 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 &^= hashWriting

3. 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.

ConcurrencyGolockingmapslicesync.Mapparameter passing
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.