- Published on
Are slices copied by value or by reference?
- Authors
Consider the following programme (Go playground link). How is it that multiplyByTwo
has amended the slice but duplicate
hasn't? How can a slice be both passed by reference (multiplyByTwo
) and passed by value (duplicate
) without using pointers?
That's exactly what we'll be walking through in this article.
func main() {
s := []int{1,2,3,4}
fmt.Println("before multiplyByTwo: ", s) // [1 2 3 4]
multiplyByTwo(s)
fmt.Println("after multiplyByTwo: ", s) // 2 4 6 8
duplicate(s)
fmt.Println("after duplicate: ", s) // 2 4 6 8
}
// multiplyByTwo accepts a slice of ints and multiplies each
// index by two.
func multiplyByTwo(s []int) {
for i, n := range s {
s[i] = n*2
}
}
// duplicate accepts a slice of ints and duplicates each entry
// exactly once
func duplicate(s []int) {
l := len(s)
for i:=0; i<l; i++ {
s = append(s, s[i])
}
}
A slice is not an array
While there are many similarities between arrays and slices (e.g. they can both be used with len
and cap
, and both types can be indexed), there are some fundamental differences.
func main() {
slc := []int{1,2,3,4,5}
arr := [5]int{1,2,3,4,5}
fmt.Printf("Slice length: %d, Slice Capacity: %d \n", len(slc), cap(slc)) // Slice length: 5, Slice Capacity: 5
fmt.Printf("Array length: %d, Array Capacity: %d \n", len(arr), cap(arr)) // Array length: 5, Array Capacity: 5
fmt.Printf("Slice First: %d \n", slc[0]) // Slice First: 1
fmt.Printf("Array First: %d \n", arr[0]) // Array First: 1
}
Note: The following is not a comprehensive guide to the differences/similarities between arrays and slices, only enough to assist with the remainder of this article.
Arrays
Arrays are fixed length. When declaring an array you state the size in-between the square brackets ([5]int{}
). When declaring the size, it becomes part of its type (e.g. [8]int
and [4]int
are different, incompatible types).
package main
func main() {
a := [5]int{1,2,3,4,5}
// Some logic here
a = [3]int{1,2,3} // Compilation Error: cannot use [3]int{...} (type [3]int) as type [5]int in assignment
}
Slices
Slices are not fixed length, do not require an initial size on initialisation (though, one can be provided) and can be extended.
package main
import (
"fmt"
)
func main() {
a := []int{1,2,3}
fmt.Printf("len: (%d) cap: (%d) \n", len(a), cap(a)) // len: (3) cap: (3)
a = append(a, []int{4,5,6}...)
fmt.Printf("len: (%d) cap: (%d) \n", len(a), cap(a)) // len: (6) cap: (6)
}
Anatomy of a slice
First, let's take a look at the slice struct.
type slice struct {
array unsafe.Pointer
len int
cap int
}
Behind every slice is an array.
- The
len
field corresponds to the slice length (the number of elements the slice contains). - The
cap
field corresponds to the slice capacity; the number of elements in the underlying array.
These fields do not always match.
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// Create slice with length of 0 and capacity of 5
slc := make([]int, 0, 5)
printSlc(slc) // len: (0), cap: (5), array pointer: (824634232608)
}
func printSlc(slc []int) {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slc))
fmt.Printf("len: (%d), cap: (%d), array pointer: (%d) \n", len(slc), cap(slc), hdr.Data) // len: (0), cap: (5), array pointer: (0xc00000c030)
}
Taking inspiration from the diagrams seen in the slices intro article, our slc
slice looks like this:
This is all important to understand because when the number of items in the slice (len
) exceeds the capacity of the underlying array (cap
) a new array needs to be created. The following example shows how the array pointer remains the same when the capacity is unchanged, but as soon as we append an additional item, exceeding the capacity, a new array is created with double the capacity and our array pointer changes to it.
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// Create slice with length of 0 and capacity of 5
slc := make([]int, 0, 5)
printSlc(slc) // len: (0), cap: (5), array pointer: (824634232608)
/*
Append 5 new items to slc, which has an underlying array with a capacity of 5, so won't need
to create a new array.
*/
slc = append(slc, []int{1, 2, 3, 4, 5}...)
printSlc(slc) // len: (5), cap: (5), array pointer: (824634232608)
/*
Append a sixth item to slc, which will exceed the underlying arrays capacity and will result in a
new array being created with a capcity double what it was.
*/
slc = append(slc, 6)
printSlc(slc) // len: (6), cap: (10), array pointer: (824635408384)
}
func printSlc(slc []int) {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slc))
fmt.Printf("len: (%d), cap: (%d), array pointer: (%d) \n", len(slc), cap(slc), hdr.Data) // len: (0), cap: (5), array pointer: (0xc00000c030)
}
This is what controls whether a function can alter the slice or not. When a function accepts a slice as a parameter, what is actually passed is the slice header (i.e. the slice struct we discussed earlier).
That means the function, via the slice header, has access to the underlying array.
If the function doesn't change the capacity of the underlying array (e.g. the earlier multiplyByTwo
example), the changes will be persisted.
If it does change the capacity of the underlying array, and thus requires a new array to be created, those changes are not persisted to the original slice and would have to be returned for the caller to use the new slice.