Go Slices
External
Internal
Overview
A slice is a descriptor, or a header that defines a contiguous segment of an underlying array, stored separately from the slice variable. A slice is not an array, a slice describes a piece of an array, and it is the idiomatic Go way of manipulating sequential, indexed data. Given that declaring a slice will create the backing array if no array is explicitly provided, you can almost always use a slice instead of an array. The slice provides access to a numbered sequence of elements from that array. A slice type denotes the set of all slices of arrays of its element type.
A slice contains a pointer to the underlying array, a length and a capacity. More details are available in the Structure section. A slice, once initialized, is always associated with the underlying array that holds its elements. The slice shares storage with its array, and other slices of the same array. A distinct slice that shares the underlying array with an original slice can be created by slicing the original slice with a slice expression.
Once the slice is initialized and the association with the underlying array is established, the association never changes, and the underlying array never changes, it cannot be grown or shrunk. However, a slice can be grown beyond the limits imposed by the original array, by allocating a new array under the covers, creating a new slice, copying elements across and using the second slice as the first slice. For more details, see append()
below.
Slices are passed by value, like an other Go variable, but when that happens, the slice argument of a function and the internal variable copy of the slice still share the underlying array.
The Go documentation used to refer to slices as reference types, but not anymore. The "reference type" terminology was removed from Go documentation. The slices are sometimes referred to as dynamic arrays.
Structure
Under the covers, a slice is a data structure that contains a pointer, the length of the slice and the capacity of the slice. These elements are referred to as contents, fields or components of the slice. You can think of it as being built like this:
type sliceHeader struct {
fistElement *T // pointer to the underlying array element that is the first element of the slice
length int
capacity int
}
Pointer
The pointer field contains a pointer to the underlying array element that represents the first element of the slice. The slice indexing is zero-based, so technically this would be slice element with the index 0.
Length
A slice's length represents the number of elements in the slice. The length of a slice is reported by the len()
built-in function.
If a slice declares that it has length 3, that means it has three elements accessible as s[0]
, s[1]
and s[2]
. Any attempt to access an element with an index lower than 0 or higher then 2 (len - 1
), for read or write, results in a panic, even if the slice has sufficient capacity:
panic: runtime error: index out of range [3] with length 3
"Length" and "size" can be used interchangeably.
Changing the Length. Do we ever change the length of a slice, or simply we create another slice, which points to the same underlying array, with a different length? For example, reslicing seems to always create a new slice. append()
updates (possibly) the copy of the slice passed as argument and returns that copy, so the original argument is never changed. It probably does not make sense to talk about changing the length of the slice. We're working with disposable copies, so it does not help to think about changing the size "in-place".
Capacity
The capacity is the maximum value the length can reach, and reflects how much space the underlying array actually has. The capacity is equal to the length of the underlying array, minus the index in the array of the first element of the slice. The capacity of a slice is reported by the cap()
built-in function.
Note that once an array is instantiated, it cannot change its size.
An existing "logical" slice can grow beyond the capacity of the underlying array by creating a new slice with a larger underlying array with append()
function.
Slices and Pass-by-Value
Go uses pass-by-value, so when a slice argument is passed to a function, the internal fields are copied across on the function stack, including the pointer to the underlying array. Therefore, the underlying array data structure is intrinsically shared, even in case of pass-by-value. Both the original slice header and the copy of the header passed to the function describe the same array. If the function changes the underlying array via its copy of the slice, the modified elements can be seen outside the function through the original slice variable.
However, if the function modifies the elements of the slice, like the length, or the pointer to the array element, those changes naturally do not propagate outside of the function. Only the changes to the underlying array do.
The slice variables are small, so their content can be copied fast. The fact that they include a pointer to the underlying array prevents copying the array, which has important performance implications, especially if the underlying array is large.
Note that slices are not reference variables, as Go does not support reference variables, so no two different slice variables may point to the same slice instance.
Pointers to Slices
It is idiomatic to use a pointer receiver for a method that modifies a slice.
nil and Empty Slice
There are both nil
and empty slices, and the difference between them is quite significant.
nil Slice
A nil
slice is a slice that has both the length and the capacity equal with zero, and the pointer to the first slice element, stored in the underlying array, equal to nil
, which indicates that there is no underlying array. Using the same imaginary struct
that helped explain slices in the Structure section, a nil
slice would be represented like this:
type sliceHeader struct {
fistElement nil
length 0
capacity 0
}
The key element that makes a slice nil
is that the first slice element is nil
, so the underlying array does not exist. There could be zero-length, zero-capacity slices that have an underlying array, like the one shown here that are not nil
slices.
A nil
slice can be instantiated with the new()
built-in function.
append()
works with nil
slices, nil
slices can be safely appended to:
var s []int
s = append(s, 1)
Test nil Slice
A slice can be tested that it is nil
with:
s == nil
which will return true
. Note that the pointer to the slice is a non-nil
pointer, which points to the handle containing the zero values. However, %p
will render "0x0".
"Almost" nil Slices
Slices can be built to have zero length and zero capacity but still be associated with an underlying array, and those slices are not nil
.
For example:
var aa [0]int
ii := aa[0:0] // zero-length, zero-capacity slice, but not a nil slice
instantiates a zero-length, zero-capacity slice that is still associated with a zero-length underlying array, so it is not a nil
slice. The s == nil
test returns false
.
The same result can be obtained with:
ii := make([]int, 0) // zero-length, zero-capacity slice, but not a nil slice
Empty Slice
An empty slice is a slice with 0 length.
An empty slice can grow, assuming it has non-zero capacity.
Declaration and Initialization
A slice can be declared with the long variable declaration.
var <sliceVarName> []<type>
var ii []int
This declaration creates a nil
slice, and it is the idiomatic way to initialize nil slices.
Inside functions, the short variable declaration can be used with make()
or a composite literal.
Initialization with make()
make()
allocates a new underlying array, and creates a new slice header to describe it, all at once.
There are two forms. In both cases, the elements of the slice are initialized with the zero value for the slice type.
One form specifies the length and the capacity of the slice:
// Create a slice with length 3 and capacity 5.
// All elements are initialized with the zero value for the slice type.
ii := make([]int, 3, 5)
A simpler form specifies only the length, with the implication that the underlying array will be created to accommodate the required length, and not more. In other words, the length will be equal with the capacity.
// Create a slice with length equal to its capacity.
// All elements are initialized with the zero value for the slice type.
ii := make([]int, 3)
Note that unlike new()
, make()
returns the slice, not a pointer.
Initialization with a Composite Literal
Initialization with a composite literal is idiomatic, unless we need a nil slice, and also has the advantage that it allows to specify the slice content, in-line. Both the length and the capacity of the resulted slice are set to the length of the composite literal. The composite literal initialization creates the underlying array.
Long variable declaration and initialization with type inference:
var ii = []int{1, 2, 3} // len 3, cap 3
ii := []int{1, 2, 3} // len 3, cap 3
Sparse Initialization
A special form of composite literal can be used when we want to initialize only some of the slice elements with constant value. <index>:<value>,...
format can be used. The last <index>:<value>
pair specifies the last element of the slice, thus determining both the length and the capacity. The elements that are not explicitly provided are initialized with the zero-value for the type.
The following example specifies a slice whose elements with the index 1, 3 and 5 are 10, 30 and 50 respectively. Because the element with the index 5 is the last element, the slice has a length of 6, and the capacity equals to the length:
ii := []int{1:10, 3:30, 5:50} // length 6, capacity 6, [0 10 0 30 0 50]
Initialization by Slicing the Underlying Array
Given an array variable, a slice of it can be declared by using the slice expression.
var aa [100]int // zero-value array declaration
var ss = aa[10:20] // slice declaration with slice expression
The "low" index is the index where the slice starts and "high" is the index where slice ends. The new slice will exclude the element indicated by the "high" index. The length of the new slice is given by the formula high - low
. However, the capacity of the new slice will be determined by the boundary of the underlying array, so it will be len(underlying_array) - low
.
Operators
Indexing Operator []
Individual slice elements can be read and written with the indexing operator []
.
ii := []int{0, 1, 2}
println(ii[0]) // prints 0
ii[0] = 10
println(ii[0]) // prints 10
A slice can only access indexes up to its length, an attempt to access an element beyond the slice length will cause a runtime panic. Even if the slice capacity exceeds its length, the elements associated to the capacity that do not belong to the slice still cannot be accessed via the index operator, they're only available for growth and an attempt to access them via the index operator will also cause a runtime panic.
Negative indexes are not supported, attempting to access a slice with a negative index generates a runtime panic.
Slice Expressions
Slice Functions
len()
len()
is a built-in function that returns the slice length. Calling len()
on a nil
slice is legal and returns 0.
cap()
cap()
is a built-in function that returns the slice capacity. Calling cap()
on a nil
slice is legal and returns 0.
copy()
copy()
accepts two arguments, both slices. It copies the data from the right-hand argument into the left-hand argument. While copying, it pays attention the the length of both arguments, and it only copies what it can without panicking, which is the minimum of the lengths of those two slices.
src := []int{10, 20, 30, 40, 50}
dest := make([]int, 3)
copy(dest, src) // no panic, only the first three elements will be copied across
copy()
can be used to copy elements around within the same slice, even if the ranges overlap. It can be used for shifting sections of the slice left or right:
s := make([]int, 5, 10)
for i := 0; i < 5; i++ {
s[i] = i + 1
}
// s is [1 2 3 4 5]
// shift elements starting with 2 right one position
copy(s[3:], s[2:])
// s is [1 2 3 3 4]
append()
The append()
built-in function appends elements to the end of a slice. If the target has sufficient capacity, it just updates its length to accommodate the new elements. If it does not, a new underlying array is allocated. append()
returns the updated slice.
append()
works with nil
slices, appending to a nil
slice just allocates a new slice.
Since the argument of the append()
function is passed by value, the changes occur on a copy of the original slice, so to make the original slice reflect the changes, it is necessary to store the result of the append()
, often in the variable holding the original slice itself, with this idiomatic pattern:
ii := []int{1, 2}
ii = append(ii, 3)
In fact, the compiler won't let you call append without saving the result.
append() as Variadic Function
append()
is a variadic function, it accepts multiple elements to be appended to the slice:
ii2 := append(ii, 1, 2, 3, 4, 5)
Appending an Entire Slice
To append a slice of the same type, use this syntax:
ii := []int{1, 2}
ii2 := []int{3, 4}
ii = append(ii, ii2...)
Idiom to insert an element on the first position in a slice:
headers := []string{"B", "C"}
headers = append([]string{"A"}, headers...)
The result with be: ["A", "B", "C"].
Appending an String to a Byte Slice
As a special case, it is legal to append a string to a byte slice (notice the ellipsis):
s := []byte("hello ")
s = append(s, "world"...)
make()
make()
is a built-in function that creates ready-to-use slices. See Initialization with make() above.
new()
new()
will initialize a nil Slice, but it will return the pointer to the slice, instead of the value.
A slice returned by new()
can be positively tested as nil
(careful to test the slice, not the pointer to it):
s := new([]int)
fmt.Println(*s == nil) // true
The slices Package
Iterating over a Slice
Use the range
keyword. range
allows to iterate by indices, values or both.
Iterate by indices:
for i := range ss {
fmt.Printf("index: %d\n", i)
}
Iterate by values:
for _, s := range ss {
fmt.Printf("element value: %s\n", s)
}
Iterate by both indices and values:
for i, s := range ss {
fmt.Printf("index: %d, element value: %s\n", i, s)
}
Iterating over a nil Slice
range
applied to a nil
slice does not panic and iterates zero times.
Sorting Slices
There are sorting functions provided by the slices
package. The sort
package also provides sorting, but the sort
documentation mentions that the slice
sorting functions run faster.
String as Slices
Multidimensional Slices
TODO:
- https://go.dev/doc/effective_go#two_dimensional_slices
- Go in Action page 100
Overlapping Slices
Note that after applying the slicing operation, the resulted slice shares a portion of the underlying array with the initial slice, so changing an element in one slice is reflected in the other. This is not necessarily a good thing, because those two slices are connected in a non-obvious way, and making a change in one slice causes a (non-obvious) change in the second slice, leading to hard to troubleshoot bugs. In order to avoid this behavior, restrict the capacity of the newly created slice with a third index. See Three-index Slice Expression.