Learning Go - Slices

Background

Quora helped me discover Golang back in the late 2015. Go was instant hit in my mind because the multi-threading code in Go was very simple and easy to understand compared to that of C++ 11. I tried to set it up and I failed multiple times but finally I was successful and I tried it a few times and left it to complete the C++ understanding. I am trying it again since the last few months and I will share about the essential topics like Slices, Pointers, Maps, Interfaces, Goroutines and the Context package. Let’s get started.

I assume you already know the history of Go (which is worth knowing) and hence I will directly jump to the topic.

Arrays

Almost all the programming language supports an in-built data structure called array. Arrays are basically a contagious allocated block of memory which allows storing a fixed size of elements into it.

Golang also supports arrays and we can create one like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main 
import "fmt"

func main() {
// define array of strings
var coll [2]string

// add values
coll[0] = "Hello"
coll[1] = "World"

// print
fmt.Println(coll)

// define and initialize arrays of numbers
nums := [3]int{1, 2, 3}
fmt.Println(nums)
}

Problem with Arrays

While arrays are helpful for string a collection there are a few caveats of using static arrays:

  • They are of fixed size and we have to provide the size while creating the arrays.
  • In golang, arrays are value type which means it’s copy by value and this impacts performance for large sized arrays.
  • In golang syntax of array can cause confusion with that of arrays and since golang promotes use of slices.

Slices

In golang, slices are actually like vector in C++. It is a resizable, referential value. This means slices can automatically expand at runtime and also it is not copied while passing to functions (which makes it performance friendly).

Let’s have a look at slices:

1
2
3
4
5
6
7
8
9
package main 

import "fmt"

func main() {
// creating an empty slice of strings
collection := []string{}
fmt.Println(collection)
}

Capacity and Length

Slices are dynamic in nature, and it has 2 properties capacity and length. The length of the slice increases on pushing items to it and decreases when we move items out of slice. The capacity is the total length of the underlying array.

We use len() function to get the length og the slice and the cap() function to get the capacity of the slice.

We can specify the capacity of the slice if we create it using make() function. And when the size of the slice is about to be exhausted, golang will create and array of double size and transfer the elements to the new array and delete the old array.

Let’s look at an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main 

import "fmt"

func main() {
t := make([]string, 3);
fmt.Println(len(t), cap(t))

t = append(t, "a", "b")
fmt.Println(len(t), cap(t))

t = append(t, "c", "d")
fmt.Println(len(t), cap(t))
}

When we will run it we will see the output like:

1
2
3
3 3
5 6
7 12

When the code executes, the slice t is created using make([]string, 3).

This allocates an underlying array of size three and produces a slice whose length and capacity are both set to three. Since no extra capacity was reserved, the slice has no room to grow without reallocation. Therefore, the first printed values are 3 3.

The next stage occurs when "a" and "b" are appended. The slice currently has a length of three and a capacity of three, meaning it is completely full. Appending any new element forces Go to allocate a new, larger underlying array. To make future appends efficient, Go does not simply increase the capacity by the number of appended elements, for smaller slices, it typically doubles the capacity.

That’s how the flow goes on 3 3 then 5 6 and finally 7 12.

Expanding Slices

We already seen above that we can use the function append(slice s, elements e1, e2, e3....) to add new elements to the slice. The append function returns a new slice everytime and the internal mechanism is also explained above.

We can append elements and even slices to an existing slice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main 

import "fmt"

func main() {
w := []string{"W", "o", "r", "l", "d"}

t := make([]string, 1);
t[0] = "H"

t = append(t, "e", "l", "l", "o", " ")

t = append(t, w...);
fmt.Println(t)
}

The output will be [H e l l o W o r l d]. We have appended the slice w to t making it a larger slice.

Iterations

To loop over the elements of the slice we can use the range function in Go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main 

import "fmt"

func main() {
t := make([]string, 1);
t[0] = "H"

t = append(t, "e", "l", "l", "o")

for _, v := range t {
fmt.Print(v)
}
}

This will print Hello in the console.

Copying

We can copy one slice to another using the copy(dest, src) function. If we are not aware of the size then we can use len() function with make() to copy a slice.

1
2
3
4
5
6
7
8
9
10
11
12
package main 

import "fmt"

func main() {
a := []int{1, 2, 3}
b := make([]int, len(a))

copy(b, a)

fmt.Println(b)
}

Re-Slicing

This is one of the important concept related to Slices in Go!.

Reslicing in Go refers to creating a new slice by adjusting the boundaries of an existing one, using the slice expression syntax s[a:b] or the full form s[a:b:c]. Reslicing allows us to shrink or expand a slice within its capacity

When we reslice, we are not creating new data; instead, we are creating a new window into the same underlying array. This means the new slice shares memory with the original, and changes to one will reflect in the other as long as they overlap in the same underlying array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
s := []int{10, 20, 30, 40, 50}
fmt.Println("original:", s, "len:", len(s), "cap:", cap(s))

// Reslice will take elements from index 1 to 3 (20, 30)
s1 := s[1:3]
fmt.Println("s1:", s1, "len:", len(s1), "cap:", cap(s1))

// Reslice further to drop the first element of s1
s2 := s1[1:]
fmt.Println("s2:", s2, "len:", len(s2), "cap:", cap(s2))

// Modify s2 which affects s and s1 because they share the same array
s2[0] = 999

fmt.Println("\nafter modifying s2:")
fmt.Println("s2:", s2)
fmt.Println("s1:", s1) // changed
fmt.Println("s :", s) // changed
}

Clearing a Slice

We can clear the slice without impaling the capacity of the lice by using this syntax s = s[:0] and to release the memory we can use s = nil.

Outro

I hope you enjoyed reading this, stay tuned for the next topic - Pointers in Go.