Another difference between slices and arrays is that assigning an array variable to another array variable creates an independent copy of the array, while assigning a slice variable to another slice variable shares the storage of the data between the two variables. This potentially becomes confusing when the slice is modified. An example on The Go Playground makes this clear:
array1 := [3]int{1, 2, 3}
array2 := array1
array2[0] = 20
fmt.Println(array1, array2) // prints [1 2 3] [20 2 3]
slice1 := []int{1, 2, 3}
slice2 := slice1
slice2[0] = 20
fmt.Println(slice1, slice2) // prints [20 2 3] [20 2 3]
A slice is a wrapper around an array (Details on how slices are implemented are in “The Difference Between Maps and Slices”). The length of the array is the capacity of the slice. When you assign one slice variable to another slice variable, you are making a copy of the wrapper, but the array is shared between the two variables until one slice is appended to a point where its capacity is larger than the initial array. Once this happens, a new backing array is allocated and the link between the two variables is broken.
This example on The Go Playground shows how the array is shared at first, but once one slice grows beyond the size of the initial array, the link is broken.
slice1 := make([]int, 0, 2)
slice1 = append(slice1, 10)
slice2 := slice1
fmt.Println(slice1, slice2) // prints [10] [10]
slice1[0] = -5
fmt.Println(slice1, slice2) // prints [-5] [-5]
slice1 = append(slice1, 20)
fmt.Println(slice1, slice2) // prints [-5 20] [-5]
slice2 = append(slice2, 30)
fmt.Println(slice1, slice2) // prints [-5 30] [-5 30]
slice1 = append(slice1, 100)
fmt.Println(slice1, slice2) // prints [-5 30 100] [-5 30]
slice2 = append(slice2, 200)
fmt.Println(slice1, slice2) // prints [-5 30 100] [-5 30 200]
In this example, slice1 is created as a slice with zero length and a capacity of two. The value 10 is set as the first value in slice1. slice2 is assigned the value of slice1, which means they keep track of size and capacity independently, but share a backing array. Setting slice1[0] to -5 changes the value for both slices. Appending a 20 to slice1 changes the size of slice1, but doesn’t change the size of slice2, which is why slice1 is now [-5 20] and slice2 is still [-5].
Things get weird when 30 is appended to slice2. Both slices have a length of two now, and since the length of the initial backing array is two, they still share it. Because of this, appending 30 to slice2 writes 30 to the shared backing array, which changes the value of slice1[1] as well.
When slice1 is appended for a third time, there’s no more room in the shared backing array, so a new backing array is created, just for slice1. The old values are copied into it, and the new value is appended to it. This breaks the connection between the two slices. When slice2 is appended, it also outgrows the initial backing array and its data is copied into a new backing array. At this point, the old backing array is no longer used and will eventually be garbage collected by the Go Runtime.
Developers rarely want this behavior. Most of the time, you want to make an independent copy of the slice. There are two ways to do this. The first is to use the built-in copy function. Look at this example, which you can run on The Go Playground:
x := []int{1, 2, 3, 4}
y := make([]int, 4)
num := copy(y, x)
fmt.Println(y, num)
You get this output:
[1 2 3 4] 4
The copy function takes two parameters. The first is the destination slice, and the second is the source slice. The function copies as many values as it can from source to destination, limited by whichever slice is smaller, and returns the number of elements copied. The capacity of x and y doesn’t matter; it’s the length that’s important.
If the destination has a smaller length than the source, only a subset of a slice is copied. The following code copies the first two elements of a four-element slice into a two-element slice:
x := []int{1, 2, 3, 4}
y := make([]int, 2)
num := copy(y, x)
The variable y is set to [1 2], and num is set to 2.
The other way to make a copy of a slice is to use the Clone function in the slices package. The Clone function takes in a slice and returns a copy.
x := []int{1,2,3,4}
y := slices.Clone(x)
Note
You might wonder why copy is a built-in function, but Clone is a function in the slices package. The reason is the copy function was defined before generics were added to Go. Without generics, you cannot write a single function that can process slices of any type. I cover generics in the related discussion.
Why does Go provide two ways to duplicate a slice? The answer is flexibility. Clone is great when you want to duplicate a slice, but the ability to provide the destination slice makes copy more powerful. Because the destination slice is provided, You have the ability to re-use the destination slice, which reduces memory usage. (Recycling isn’t only good in the real world!) You can even use copy to duplicate the middle or end of a slice by passing only parts of the source and destination slices to copy. How do you read from only part of a slice? It’s time to learn about slice expressions.