Go Basics: Slices

This article primarily introduces the data structure slice in 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:

1
2
3
4
5
6
7
func arraySum(arr [3]int) {
	sum := 0
	for _, v in range arr{
        sum = sum + v
	}
	return sum
}

This sum function only supports the [3]int type; no other types are supported.

Another example:

1
var a = [3]int{1, 2, 3}

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:

1
var name []T // T-finger type.
  • name: the variable name
  • T: the type of elements in the slice

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
	i := 10
	// Declaration of slice type
	var a []string              // Declare stanning type slices
	var b = []int{}             // Declaration of int type slices, initialization
	var c = []bool{true, false} // Declare and initialize slices of the bool type
	var d = []*int{&i, nil}     // Declare an integer pointer type and initialize
	var e = []bool{true, false} // Declare a Bool type slice and initialize it
	fmt.Println(a)              // []
	fmt.Println(b)              // []
	fmt.Println(c)              // [true false]
	fmt.Println(d)              // [0xc0000aa058 <nil>]
	fmt.Println(a == nil)       // true
	fmt.Println(b == nil)       // false
	fmt.Println(c == nil)       // false
	fmt.Println(d == nil)       // false
	fmt.Println(c == e)         // Slices are not a value type and can only be compared with nil. Invalid operation: c=e
}

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.

1
2
3
4
5
var a []int
fmt.Printf("len(a):%v, cap(a):%v\n", len(a), cap(a)) // len(a):0, cap(a):0

b := []int{1, 2, 3}
fmt.Printf("len(b):%v, cap(b):%v\n", len(b), cap(b)) // len(b):3, cap(b):3

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:

  1. A simple slice expression, var a = b[low:high], which has two index bounds.
  2. 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

1
var a = b[low:high]

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.

1
2
3
4
5
func main(){
    a := [5]int{1, 2, 3, 4, 5}
    s := a[1:4]
    fmt.Printf("s:%v, len(s):%v, cap(s):%v\n", s, len(s), cap(s))
}

Output:

1
s:[2 3 4], len(s):3, cap(s):4

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:

1
2
3
a[:3] 等同于 a[0:3]
a[1:] 等同于 a[1:len(a)]
a[:]  等同于 a[0:len(a)]

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.

1
2
3
4
5
6
7
8
func main(){
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]
	fmt.Printf("s:%v, len(s):%v, cap(s):%v\n", s, len(s), cap(s))
	
	s1 := s[3:4]
    fmt.Printf("s1:%v, len(s1):%v, cap(s1):%v\n", s1, len(s1), cap(s1))
}

Can you guess what the program will output? Will s1 cause an index-out-of-range error?

The output is as follows:

1
2
s:[2 3], len(s):2, cap(s):4
s1:[5], len(s1):1, cap(s1):1

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:

1
a[low:high:max]

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.

1
2
3
4
5
6
7
func main() {
	a := [6]int{1, 2, 3, 4, 5, 6}
	s1 := a[1:3]    // Normal Slice Expression
	s2 := a[1:3:5]  // Full Slice Expression
	fmt.Printf("s1:%v, len(s1):%v, cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v, len(s2):%v, cap(s2):%v\n", s2, len(s2), cap(s2))
}

The output is as follows:

1
2
s1:[2 3], len(s1):2, cap(s1):5
s2:[2 3], len(s2):2, cap(s2):4

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:

1
make([]T, size, cap)

Where:

  • []T: the slice type
  • size: the slice length
  • cap: the allocated capacity

For example:

1
2
3
4
5
func main(){
	a := make([]int, 2, 10)
	// a:[0 0], len(a):2, cap(a):10
	fmt.Printf("a:%v, len(a):%v, cap(a):%v", a, len(a), cap(a))
}

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:

  1. A pointer to an array
  2. The slice length len
  3. 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:

image-20210820161940647

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

image-20210820163022314

What happens if we now change the second element of s2 to 100?

1
2
3
4
5
6
7
8
9
func main() {
	a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := a[:5]
	s2:= s1[1:3]
	fmt.Printf("s1:%v, len(s1):%v, cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v, len(s2):%v, cap(s2):%v\n", s2, len(s2), cap(s2))
	s2[1] = 100
	fmt.Printf("a:%v, s1:%v, s2:%v\n", a, s1, s2)
}

The result is as follows:

1
2
3
4
5
s1:[0 1 2 3 4], len(s1):5, cap(s1):8

s2:[1 2], len(s2):2, cap(s2):7

a:[0 1 100 3 4 5 6 7], s1:[0 1 100 3 4], s2:[1 100]

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.

1
2
3
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	a := [3]int{10, 20, 30}
	// S1 slices obtained from a array of slice expressions
	s1 := a[:]
	// Get S2 slices by giving copies
	s2 := s1
	fmt.Printf("s1:%v, len(s1):%v, cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v, len(s2):%v, cap(s2):%v\n", s2, len(s2), cap(s2))
	s2[0] = 100
    fmt.Println()
	fmt.Printf("a:%v\ns1:%v\ns2:%v", a, s1, s2)
}

The output is as follows:

1
2
3
4
5
6
s1:[10 20 30], len(s1):3, cap(s1):3
s2:[10 20 30], len(s2):3, cap(s2):3

a:[100 20 30]
s1:[100 20 30]
s2:[100 20 30]

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	s := []int{1, 2, 3, 4, 5}
	// Through indexing
	for i := 0; i < len(s); i++{
		fmt.Printf("index:%d, value:%d\n", i, s[i])
	}
	// Go through it through the forrange.
	for i, v := range s {
		fmt.Printf("index:%d, value:%d\n", i, v)
	}
}

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.

1
2
3
4
5
6
7
8
func main() {
	s1 := []int{1, 2, 3}  // [1 2 3]
	s1 = append(s1, 4)    // [1 2 3 4]
	s1 = append(s1, 5, 6) // [1 2 3 4 5 6]
	a := [3]int{7, 8, 9}
	s1 = append(s1, a...) // Unable to compile, no arrays
	s1 = append(s1, []int{7, 8, 9}...) // [1 2 3 4 5 6 7 8 9]
}

Note: Elements can be added directly with append() to a zero-value slice declared using the var keyword. The append function will allocate memory.

1
2
3
4
5
func main(){
    var s []int
    s = append(s, 1, 2, 3)
    fmt.Printf("s:%v\n", s) // [1 2 3]
}

Although there is nothing wrong with the following code, it is unnecessary:

1
2
3
4
5
6
// There's no need for initialization.
s := []int{}
s = append(s, 1, 2, 3)
// There's no need for initialization.
s1 := make([]int)
s1 = append(s1, 1, 2, 3)

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.

Built with Hugo
Theme Stack designed by Jimmy