Arrays and Slices are one of the fundamental and important concepts of Go programming language. They both contain sequence of types but with some differences.
Arrays
Like many other programming languages, arrays in Go are fixed sized containers. An array starts with index 0 and its elements can be assigned or reached directly.
var array [2]int
fmt.Println(array)
array[0]=3
array[1]=4
fmt.Println(array[0])
fmt.Println(array[1])
fmt.Println(array)
[0 0]
3
4
[3 4]
Notice that, the values in an empty array are equal to the default value of the type it has. If it was an string array, its default values would be an empty string(“”).
var array [2]string
fmt.Println(array[0]=="" && array[1]=="" )
true
So what is a slice?
Although arrays are very useful, they can not be resized later. This is where slices come into play. They are dynamically sized containers. They can be created like arrays without exact size. You can then add values using built-in “append” function.
var slice []int
fmt.Println(slice)
slice = append(slice, 3)
fmt.Println(slice[0])
slice = append(slice, 4)
fmt.Println(slice[1])
fmt.Println(slice)
[]
3
4
[3 4]
As you can see, empty slice values are nil and reaching its elements before assigning a value will give an error.
var slice []int
fmt.Println(slice[0])
panic: runtime error: index out of range [0] with length 0
It is getting complicated
So far everything seems simple, but slices under the hood are designed as a wrapper of an array. When you create a slice, first a new array is created and then assigned to that slice. A slice consist of 3 parts: a pointer to an array, a length and a capacity.
- Pointer: The pointer points to the actual array which holds the values
- Length(len): The length(size) of the slice
- Capacity(cap): The capacity(size) of the underlying array.
The capacity is used to manage memory effectively. This value can be considered as a threshold. If passed, the capacity is recalculated and the array is copied to a larger area(relative to new capacity) in memory. In general, the new capacity is double its previous size.
Built-in function “make” can be used to create a slice with a predefined capacity
func make([]T, len, cap) []T
As you can see in the example below, the address of the slice does not change until the length of the slice reaches its capacity. The array then copies its values into another array with twice the capacity of the previous one.
slice := make([]int, 0, 3)
slice = append(slice, 1)
fmt.Printf("%p, %v, len: %v, cap: %v \n", array, array, len(array), cap(array))
slice = append(slice, 2)
fmt.Printf("%p, %v, len: %v, cap: %v \n", array, array, len(array), cap(array))
slice = append(slice, 3)
fmt.Printf("%p, %v, len: %v, cap: %v \n", array, array, len(array), cap(array))
slice = append(slice, 4)
fmt.Printf("%p, %v, len: %v, cap: %v \n", array, array, len(array), cap(array))
0xc000014018, [1], len: 1, cap: 3
0xc000014018, [1 2], len: 2, cap: 3
0xc000014018, [1 2 3], len: 3, cap: 3
0xc000100030, [1 2 3 4], len: 4, cap: 6
Sub slicing
A slice can also be created via slicing another slice or an array. An expression array[e1,e2] can be used to slice an array/slice from element e1 to element e2.
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[2:4]
fmt.Println(slice2)
slice1[2]=7
fmt.Println(slice2)
[3 4]
[7 4]
Once again, to effectively manage memory, both slice1 and slice2 share the same base array, so any changes made to the original array will be reflected in slices created from it.
For this reason, sometimes you may want to copy the slice to another slice when necessary to avoid side effects.
As can be seen, slices are much more useful than arrays, but care should be taken, especially when sub-slicing, as data is shared.