How Go Pass Values: Slice Vs Map
As we all know, Go is a statically typed language that uses pass-by-value semantics. This means that when you pass a variable to a function, Go creates a copy of that variable. However, the behavior of passing slices and maps can be a bit confusing due to their underlying implementations.
For example, when you pass the slice, sometimes the change inside the function reflects outside the function, and sometimes it doesn’t. But for the map, the change always reflects outside the function. Let’s explore why this happens.
Map Passing Mechanism
In Go, a map is a reference type. When you pass a map to a function, you are passing a copy of a pointer to the hmap instead of the hmap itself. This means that both the original map and the map inside the function point to the same hmap struct.
func growMap(m map[string]int) {
// Add enough elements to trigger growth
for i := 0; i < 100; i++ {
m[fmt.Sprintf("key%d", i)] = i
}
}
func main() {
myMap := make(map[string]int)
myMap["original"] = 1
growMap(myMap) // This WILL be reflected outside!
fmt.Println(len(myMap)) // Prints: 101 (not 1!)
fmt.Println(myMap["key50"]) // Prints: 50
}
From above example, we can see that the changes made to the map inside the growMap function are reflected outside the
function.
Even if the map grows and rehashes, the original map in main still points to the same underlying hmap, because in this
case,
we just modify the bucket array inside the hmap.
// In memory layout:
var m map[string]int = make(map[string]int)
// Stack:
[main function stack frame]
m: 0x5000 ────────────┐
│
// Heap: ▼
[0x5000] hmap {
count: 3,
B: 1,
buckets: 0x6000 ──────┐
hash0: 0x12345678, │
... │
} ▼
[0x6000] bucket array [bmap0][bmap1]
Each bmap: [tophash×8][keys×8][values×8]
Slice Passing Mechanism
When we pass a slice to a function, we are passing a copy of the slice header, which contains a pointer to the underlying array. This means that both the original slice and the slice inside the function point to the same underlying array. However, if the slice grows and needs to allocate a new underlying array, the original slice will still point to the old array, while the slice inside the function will point to the new array.
func growSlice(s []int) {
fmt.Printf("Function before: Data=%p, Len=%d, Cap=%d\n",
unsafe.Pointer(&s[0]), len(s), cap(s))
// Force reallocation
for i := 0; i < 100; i++ {
s = append(s, i)
}
fmt.Printf("Function after: Data=%p, Len=%d, Cap=%d\n",
unsafe.Pointer(&s[0]), len(s), cap(s))
}
func main() {
s := []int{1, 2, 3}
fmt.Printf("Main before: Data=%p, Len=%d, Cap=%d\n",
unsafe.Pointer(&s[0]), len(s), cap(s))
growSlice(s) // Pass SliceHeader by value
fmt.Printf("Main after: Data=%p, Len=%d, Cap=%d\n",
unsafe.Pointer(&s[0]), len(s), cap(s))
}
// Output:
// Main before: Data=0x14000018180, Len=3, Cap=3
// Function before: Data=0x14000018180, Len=3, Cap=3 ← Same array
// Function after: Data=0x14000020000, Len=103, Cap=128 ← NEW array!
// Main after: Data=0x14000018180, Len=3, Cap=3 ← OLD array unchanged!
- Function’s append() creates new array and updates local SliceHeader
- Main’s SliceHeader still points to original array
- No shared state between the two SliceHeaders
Summary
- When we pass the map, we pass a copy of the pointer to the hmap, so changes inside the function reflect outside.
- When we pass the slice, we pass a copy of the SliceHeader, so changes inside the function may not reflect outside if the slice grows and reallocates.