This article primarily introduces the data structure
slicein Go and its basic usage.
Introduction
Previously, we introduced arrays in Go. The length of an array is fixed and is part of its type, so arrays have many limitations. For example, consider summing an array:
| |
This sum function only supports the [3]int type; no other types are supported.
Another example:
| |
a can contain at most three elements, and no additional elements can be added.
Slices
A slice is a variable-length data structure consisting of elements of the same type. It is an abstraction over array types. Slices are highly flexible and support dynamic capacity expansion.
A slice is a wrapper around an array and is a reference type rather than a value type. Internally, a slice contains a capacity, length, and pointer.
Defining a Slice
The syntax for declaring a slice is as follows:
| |
- name: the variable name
- T: the type of elements in the slice
Example:
| |
Slice Length and Capacity
As mentioned earlier, a slice internally encapsulates its capacity and length. We can use Go’s built-in len() function to obtain the length of a slice and the cap() function to obtain its capacity.
| |
Slice Expressions
A slice expression can be written as var a = b[low:high], where the variable b can be a string, array, slice, or a pointer to an array or slice.
A slice expression is syntax for constructing a substring or subslice from a string, array, array pointer, or slice. (Note: Applying a slice expression to an array produces a slice.)
There are two forms of slice expressions:
- A simple slice expression,
var a = b[low:high], which has two index bounds. - A full slice expression,
var a = b[low:high:size], which specifies an index that determines the slice capacity in addition to the two index bounds.
Simple Slice Expressions
A slice is a wrapper around an array, with an array as its underlying storage. Therefore, we can use a slice expression to obtain a slice from an array. In the slice expression
| |
low and high represent the lower bound (inclusive) and upper bound (exclusive), respectively. The slice length is high - low, and the slice capacity is determined by the length of the operand.
In the following code, the slice s contains the elements at index positions 1 <= index < 4 of the array a. Its length is 4 - 1 = 3, and its capacity is based on the array length 5.
| |
Output:
| |
For convenience, slice expressions allow the values of low and high to be omitted. If low is omitted, low defaults to 0. If high is omitted, high defaults to the length of the operand. That is:
| |
Note
For arrays or strings, 0<=low<=high<=len(a) must be satisfied; otherwise, an out-of-bounds error will occur.
When applying a slice expression to a slice s (slicing a slice), the maximum value of high is not the length, but the capacity of s. Indices must be non-negative and representable by a value of type int. For arrays or constant strings, constant indices must also be within the valid range. If both low and high are constants, they must satisfy low <= high. If an index is out of range at runtime, a runtime panic occurs.
| |
Can you guess what the program will output? Will s1 cause an index-out-of-range error?
The output is as follows:
| |
There is no index-out-of-range error, and the number 5, which is not present in s, is printed. This is because s1 is obtained by slicing the slice s, and the maximum value of high in the slice expression is the capacity of s, which is 4, so the index is not out of bounds.
Full Slice Expressions
The syntax for a full slice expression is:
| |
Full slice expressions can be used with arrays, pointers to arrays, and slices (note: they cannot be used with strings). A full slice expression works like a simple slice expression, except that it sets the final slice capacity to the value of max - low. The resulting slice has a length of high - low and a capacity of max - low.
In a full slice expression, only low can be omitted. If omitted, it defaults to 0.
| |
The output is as follows:
| |
A full slice expression must satisfy 0 <= low <= high <= max <= cap(a). Its other properties are the same as those of a simple slice expression.
Creating Slices with the make() Function
Earlier, we created a slice from an array using a slice expression and also introduced how to create and initialize a slice (var a = []int{1, 2, 3}). Neither approach allows the slice capacity to be specified dynamically. The following section explains how to dynamically create a slice using the built-in make() function. The syntax is as follows:
| |
Where:
- []T: the slice type
- size: the slice length
- cap: the allocated capacity
For example:
| |
As you can see, the slice capacity—that is, the allocated storage space—is 10, but only 2 elements are in use, so the slice length is 2. In fact, the memory allocated for the underlying array has a length of 10.
The Nature of Slices
A slice is essentially a wrapper around an array. Internally, a slice contains:
- A pointer to an array
- The slice length
len - The slice capacity
cap
Suppose there is an array a := [8]int{0, 1, 2, 3, 4, 5, 6, 7} and a slice s1 := a[:5], and s1 is sliced again to produce s2 := s1[1:3]. The slices can be illustrated as follows:

Because s2 is obtained by slicing s1, they share the same underlying array, as illustrated below:

What happens if we now change the second element of s2 to 100?
| |
The result is as follows:
| |
We can see that the values at the corresponding positions in array a and slices s1 and s2 have all changed to 100. This demonstrates that the underlying arrays of s1 and s2 are both array a. This is something we need to keep in mind. If we do not want to modify the original array or slice during development, we should use the copy() function to make a copy.
Slice Characteristics
Slices Cannot Be Compared Directly
Slices are reference types, so we cannot use the == operator to determine whether the elements in two slices are exactly equal. The only valid comparison operation for a slice is comparing it with nil. When a slice is nil, it has no underlying array. A slice with a value of nil has both a length and capacity of 0.
How Do You Determine Whether a Slice Is Empty?
First, what does “empty” mean? Generally, when determining whether a slice is empty, we want to determine whether the slice contains any data.
We know that a slice is a reference type and can only be compared with nil. When a slice equals nil, it means no memory has been allocated for the slice. In this case, the slice can certainly be considered empty.
However, there is another case: memory has been allocated, but the slice’s len value is 0. Although space has been allocated for the underlying array, the slice does not contain any data. According to the definition above, the slice should also be considered empty.
| |
When a slice is nil, len(s) also returns 0. Therefore, to determine whether a slice is empty, use len(s) == 0 rather than s == nil.
Assigning and Copying Slices
When a slice is copied through assignment, both slices share the same underlying array. Therefore, modifying an element in the copied slice also changes the corresponding value in the original array. For example:
| |
The output is as follows:
| |
We can see that after changing the value of element 0 in s2, the values of element 0 in array a and slice s1 also become 100. This demonstrates that:
- When an array is sliced, the array becomes the underlying array of the new slice.
- When a slice is copied through assignment, the new slice and the original slice share the same underlying array.
After copying a slice through assignment, modifying the new slice affects the values in the original slice. This requires special attention.
Iterating Over Slices
Slices can be iterated over in the same way as arrays. Two iteration methods are supported: index-based iteration and for range iteration.
| |
Adding Elements with the append() Function
Go’s built-in append() function can dynamically add elements to a slice. You can add a single element, multiple elements, or append another slice to the end of the current slice by adding ... after the argument. Note that the argument cannot be an array; it must be another slice.
| |
Note: Elements can be added directly with append() to a zero-value slice declared using the var keyword. The append function will allocate memory.
| |
Although there is nothing wrong with the following code, it is unnecessary:
| |
Every slice points to an underlying array. If the array has enough capacity, new elements are added to it. When the underlying array cannot accommodate the new elements, the slice automatically grows according to a certain strategy, and its underlying array is replaced. This “capacity expansion” often occurs when the append() function is called, so we need to assign the value returned by the append() function back to the original variable.