Common gotchas when mutating a slice
Common gotchas when mutating a slice
Mutating slices in Go can be a bit confusing.
It’s generally known that slices are a reference an underlying array. But when it comes to mutating a slice, there are some very common gotchas that can trip you up. Let’s look at three scenarios.
From inside a function
In Go, all variables are passed by value, meaning the function body will receive a copy.
When you pass a slice to a function, the variable (slice descriptor), is a copy of the caller’s reference to the underlying array (by caller
I mean the place where the function is being called).
So if you try to append to the slice inside the function, you are actually appending to a duplicate of the original reference. The caller’s slice will remain unaffected.
In the example below, we try to append a value to a slice, using the addNum
function. Unfortunately, after calling addNum
, the slice hasn’t changed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// THIS WON'T WORK
mySlice := []int{1,2,3}
func addNum (s []int, num int) {
s = append(s, num)
}
addNum(mySlice, 4)
fmt.Println(mySlice)
// --> [1 2 3]
To successfully append to the slice, the function must return its copy of the slice. You then assign it to the original slice.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mySlice := []int{1,2,3}
func addNum (s []int, num int) []int {
return append(s, num)
}
mySlice = addNum(mySlice, 4)
fmt.Println(mySlice)
// --> [1 2 3 4]
From inside a for-loop
When ranging over a slice, the variable referencing each value points to a copy of the value. If you try to mutate the value at a given index with this variable, it will not work. Once you leave the scope of the for-loop, you’ll find the slice was unaffected.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// THIS WON'T WORK
somePeople := []string{"Ada", "Grace", "Charles"}
for _, name := range somePeople {
name = strings.ToUpper(name)
}
fmt.Println(somePeople)
// --> ["Ada" "Grace" "Charles"]
For the mutation to stick, you must use bracket notation instead:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
somePeople := []string{"Ada", "Grace", "Charles"}
for i, name := range somePeople {
somePeople[i] = strings.ToUpper(name)
}
fmt.Println(somePeople)
// --> ["ADA" "GRACE" "CHARLES"]
When using variables
This is similar to the for-loop example. Any changes to a slice require the use of bracket notation. Variables will point to copies of the value at a given index, meaning any changes to that variable will not affect the slice.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// THIS WON'T WORK
type food struct {
name string
}
pantry := []food{
{name: "banana"},
{name: "satsuma"},
{name: "kiwi"},
}
toChange := pantry[2] // this makes a copy!
toChange = food{name: "strawberry"}
fmt.Println(pantry)
// --> [{banana} {satsuma} {kiwi}]
With bracket notation, it works.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type food struct {
name string
}
pantry := []food{
{name: "banana"},
{name: "satsuma"},
{name: "kiwi"},
}
pantry[2] = food{name: "strawberry"}
fmt.Println(pantry)
// --> [{banana} {satsuma} {strawberry}]