Slices are one of the stranger pieces of Go. They're like lists or vectors in other languages, but have some peculiar behaviors; particularly when multiple slices share the same backing array. I suspect that a lot of bugs will come from slices that suddenly stop sharing.
To explain, let's start with a simple slice example: creating two slices backed by an explicit array (you can run these examples in the Go Playground):
package main import "fmt" func main() { a := []int{1, 2, 3, 4, 5} s1 := a[1:4] s2 := s1[0:2] fmt.Println(a) fmt.Println(s1) fmt.Println(s2) }
When you run this program, you get the following output (the slice operator is inclusive of its first parameter, exclusive of its second):
[1 2 3 4 5] [2 3 4] [2 3]
As I said, these slices share a backing array. A change to s2
will be reflected
in s1
and a
:
func main() { a := []int{1, 2, 3, 4, 5} s1 := a[1:4] s2 := s1[0:2] s2[0] = 99 fmt.Println(a) fmt.Println(s1) fmt.Println(s2) }
[1 99 3 4 5] [99 3 4] [99 3]
If you're used to slices from, say, Python, this is a little strange: Python slices are separate objects. A Go slice is more like a Java sub-list, sharing the same backing array. But wait, there's more, you can add items to the end of a slice:
func main() { a := []int{1, 2, 3, 4, 5} s1 := a[1:4] s2 := s1[0:2] s2 = append(s2, 101) fmt.Println(a) fmt.Println(s1) fmt.Println(s2) }
Since s2
shares backing store with s1
and a
, when you append
a value to the former, the latter are updated as well:
[1 2 3 101 5] [2 3 101] [2 3 101]
But now what happens if we append a bunch of values to s2
?
func main() { a := []int{1, 2, 3, 4, 5} s1 := a[1:4] s2 := s1[0:2] s2 = append(s2, 101) s2 = append(s2, 102) s2 = append(s2, 103) fmt.Println(a) fmt.Println(s1) fmt.Println(s2) }
[1 2 3 101 102] [2 3 101] [2 3 101 102 103]
Did you see that one coming? Here's one more piece of code to ponder:
func main() { a := []int{1, 2, 3, 4, 5} s1 := a[1:4] s2 := s1[0:2] s2 = append(s2, 101) s2 = append(s2, 102) s2 = append(s2, 103) a[3] = 17 fmt.Println(a) fmt.Println(s1) fmt.Println(s2) }
[1 2 3 17 102] [2 3 17] [2 3 101 102 103]
As you can see, s2
no longer uses a
as its backing array. Not only
does a
not reflect the last element added to the slice, but changing an element
of a
does not propagate to s2
.
This behavior is hinted in the slice internals documentation, which says that append()
“grows
the slice if a greater capacity is needed.” Since Go requires you to pay attention to return
values, whatever code appended to the slice will always see the correct values. But if you have multiple
slices that you think refer to the same backing array, the others won't.
I can understand why you would want slices to share a backing array: they represent a view on that
array. And I can understand the desire for expanding the slice via append()
: it's the
behavior that other languages provide in a list. But this blend seems to be the worst of both worlds,
in that you never know whether a changing a given slice will mutate other slices/arrays, or not. I
recommend treading carefully.
No comments:
Post a Comment