cover image for article

Golang: Slices

#go

Introduction

In the sixth part of the series, we will be covering slices. Slices are almost like arrays but have a lot of advantages over them, including flexibility and control over them. We can adjust the size and capacity of the data which we will store at a place using slices. We will be covering basic declaration, initialization, capacity in slices, iteration, and accessing the elements of the slices.

Slices in Golang

Slices are Arrays but can provide more control and are more flexible than arrays. In slices, we can increase the size of the array/list of elements whenever required. We can even have a capacity for slices i.e. the maximum length we wish to grow the initial slice.

Though slices are dynamic, it has a few disadvantages like compile safety, access time, comparability, etc. Everything has its pros and cons, you have to decide on the right data structure as per your problem statement and requirements.

Declaring Slices

There are a couple of different ways in which we can declare a slice that might be an uninitialized or initialized slice. Some of the standard ways include using the make function and the normal array-like declaration though there are other methods as well including using the new function. We'll explore some of the most easiest and handy ways to declare and initialize slices.

Using array-like declaration

Slices can be declared quite similar to arrays but we don't enter the initial size(length). As discussed in the array part, we can use the var keyword and the square brackets. Though you don't have to enter the length inside the [] brackets, the type of the slice needs to enter.

package main

import "fmt"

func main() {
    var marks [] int
    fmt.Println(marks)
}
$ go run slices.go
[]

So, initially, it is empty without an element as we have not initialized any elements. If you enter the length in the [] brackets, it would be an array and not a slice.

We can also initialize the elements in the slice with the slice literal as we did with arrays using an array literal.

frameworks = []string{"Django", "Laravel", "Flask", "Rails"}
fmt.Println(frameworks)
$ go run slices.go
[Django Laravel Flask Rails]

Using make function

We can even use the make function to create a slice in golang. The make function basically allows us to create a slice by providing the length as well as the capacity. Let's clear the concept of Capacity and Slice first.

Capacity in Slice

Capacity in slices is the initial value provided during the declaration of a slice. It basically holds the capacity of the slice to grow beyond the length. OK, let's take an example, If you declare a slice using make with length 3 and capacity 5, you initially can access 3 elements but memory has been allocated for 5 elements, if your slice exceeds this capacity it will double its original capacity.

Obtain Length and Capacity of Slice

We can get the capacity by using the cap function just like the len function. In slices, we can use the lens function to get the length and the cap function to get its underlying capacity to grow.

It's just a way for us to manage the memory reallocation for a slice. Slices under the hood are arrays with a more dynamic punch. So, now we can a bit confidently move to the make function for declaring slices.

Back to make function

The Make function is used to declare and initialize a slice (not only slice but maps and channels as well). The function primarily takes in 3 parameters namely, the type of slice, the initial length of the slice, and optionally the capacity of the slice. If we don't provide the capacity, the capacity is set the same as the length.

var langs = make([]string, 3, 5)

langs[0], langs[1], langs[2] = "Python", "Go", "Javascript"
fmt.Println(langs)

fmt.Printf("Length = %d \nCapacity = %d\n", len(langs), cap(langs))

langs = append(langs, "Java", "Kotlin", "PHP")

fmt.Println(langs)
fmt.Printf("Length = %d \nCapacity = %d\n", len(langs), cap(langs))
$ go run slices.go
[Python Go Javascript]
Length = 3
Capacity = 5
[Python Go Javascript Java Kotlin PHP]
Length = 6
Capacity = 10

So, there are a lot of things to take in here. We use the make function by parsing in three parameters as said the type in the form of []type as a distinction for creating slices because we also use the map to create maps and channels. The next two parameters are length and capacity. So, we have initialized a slice of type string, length 3 i.e. we are saying we will initially access only three elements from the array, finally, the third parameter is the capacity which will be the already initialized array under the hood for the slice. So, we have already created an array(under the hood) with 5 elements initialized but only 3 accessible from the slice interface.

Further, we initialize/modify the elements in the created slice. We set 3 elements to some strings and that way we have all elements filled with non-default values in the slice. Now if we say langs[3] = "Something" it would give an error like panic: runtime error: index out of range [3] with length 3 . This is a panic in golang which can be triggered in one of the ways in the slice when you access an unreferenced element in it. We have initialized the element but not in the slice interface. So, we have a particular function called append which appends and thus grows the length beyond its current length and refers to the elements initialized from the capacity.

So initially the capacity was 5 and after adding the 6th element, it doubled down to 10 . So, under the hood, all 5 elements in the array would have been re-allocated into a new memory location and the sixth element would have been added to the next location. This is how we efficiently re-allocate memory for elements in slices. We have a bit more control over the length and are more flexible than arrays.

Using new function

The new function in golang is used to allocate a slice/any other type. We can use the new function so as to mimic the make function by adding a bit of value like the initial capacity and length. You can refer to the article for providing the original idea.

langs2 := new([3]string)[0:2]

langs2[0], langs2[1] = "Python", "Go"
fmt.Println(langs2)

fmt.Printf("Length = %d \nCapacity = %d\n", len(langs2), cap(langs2))

langs2 = append(langs2, "Java", "Kotlin", "PHP")

fmt.Println(langs2)
fmt.Printf("Length = %d \nCapacity = %d\n", len(langs2), cap(langs2))
$ go run slices.go
Length = 2
Capacity = 3
[Python Go Java Kotlin PHP]
Length = 5
Capacity = 6

So, it would work almost similar to the make function. But by default, it would work as nil if you don't provide any length in the [] brackets like new([]string) . This will create a empty slice [] with zero capacity and zero length.

Adding elements in Slice

We had a few spoilers for this already with the append function. The append takes in the variable and then the list of values that we want to add. Here, if the capacity of the slice is exceeded, it re-allocates the slice to a new location, and the elements are moved to that location and then the provided elements are added.

var percentages = []float64{78.8, 85.7, 94.4, 79.8}
fmt.Println(percentages)
percentages = append(percentages, 60.5, 75.6)
fmt.Println(percentages)
$ go run slices.go
[78.8 85.7 94.4 79.8]
[78.8 85.7 94.4 79.8 60.5 75.6]

So, here we can see the append function adding the elements in a slice. You can pass as many elements(MaxInt i.e. int64 or int32 elements precisely) you require in the append function. Calling the append function, again and again, might degrade the efficiency though, so make sure to add a right number of elements in a single call.

Accessing and Modifying elements in Slice

We can simply access the elements using the index in the [] brackets. But there is more to that in slices. We can actually get slices of slices. Even in arrays or slices, we can get the particular elements between a specific two indices like 2 and 4, so we can write array[2:4] to get elements at index 2 , and 3 the upper bound is non-inclusive. But if we want to have all the elements from a specific index to the last element, we can leave the number blank as [2:] would give elements from index 2 to the last index in the array/slice.

scores := []int{80, 85, 90, 75, 60, 56, 83}
fmt.Println(scores)
fmt.Println("From index 2 to 4", scores[2:5])
fmt.Println("From index 0 to 2", scores[:3])
fmt.Println("From index 3 to 5", scores[3:])
$ go run slices.go
[80 85 90 75 60 56 83]
From index 2 to 4 [90 75 60]
From index 0 to 2 [80 85 90]
From index 3 to 5 [75 60 56 83]

So, we are able to perform index slicing in golang on arrays and slices. Further, as for the array, we can also modify elements in slices. Using the index of that element, we can access the element and perform operations on it and thus change the literal value of the element.

word := []byte{'f', 'u', 'z', 'z', 'y'}
fmt.Printf("%s\n", word)
word[0] = 'b'
word[len(word)-1] = 'z'
fmt.Printf("%s\n", word)
fuzzy
buzzz

So, now we can also modify existing values of elements in slices.

Deleting elements from Slice

We can also remove an element from the slice i.e. shrink the length of the slice. There is no function to remove an element from the slice, but we can work around with the append function in golang. So, in the slice before the element's index to be deleted is appended with all the elements after the index of the element to be deleted.

10 20 30 40 50 60
0  1  2  3  4  5

Delete the element at index -> 2

Copy from 3 to 5 into the slice from 0 to 1

// append(slice[:2], slice[2+1:]
            ^            ^
            |            | 
          10 20       40 50 60        

append 40 50 60 -> 10 20

10 20 40 50 60

Here in the above example, we append the indices after the element to be deleted into the slice of elements before the to be deleted element .

marklist := []int{80, 85, 90, 75, 60}
fmt.Println(marklist)

var index int
fmt.Printf("Enter the index to be deleted: ")
fmt.Scan(&index)

elem := marklist[index]


// append in such a way that the element to be removed is excluded

marklist = append(marklist[:index], marklist[index+1:]...)


fmt.Printf("The element %d was deleted.\n", elem)
fmt.Println(marklist)
$ go run slices.go

[80 85 90 75 60]
Enter the index to be deleted: 3
The element 75 was deleted.
[80 85 90 60]

Iterate through a slice

As arrays are under the hood modifications of arrays, we have a quite similar approach to iterating over slices in golang.

Using three statements for loop

We can use the three statements for loop i.e. the initialization, condition, and incrementation procedure. The counter is set from 0 or any other starting value as the index of the slice, next we have the end loop condition i.e. a condition to check until when to exit, and finally the amount to which we need to increment the counter.

code := [7]rune{'g', 'o', 'l', 'a', 'n', 'g'}
for i := 0; i < len(code); i++ {
    fmt.Printf("%c\n", code[i])
}
$ go run slices.go
g
o
l
a
n
g

Using Range-based for loop

We can use range-based for loops to iterate over the slice elements. The range keyword is passed with the slice name or the slice of an array to iterate over. Using the two variables i.e. the iterator and the copy of the element, we can access the index and the element in the slice.

scores := []int{80, 85, 90, 75, 60, 56, 83}
for _, s := range scores {
    fmt.Println(s)
}
$ go run slices.go
80
85
90
75
60
56
83

We can also use a slice of slice i.e. scores[:4] to access a specific element in the range using index slicing.

scores := []int{80, 85, 90, 75, 60, 56, 83}
for _, s := range scores[1:4] {
    fmt.Println(s)
}
$ go run slices.go
85
90
75

Using for loop with range

We can even use them for loop as a while loop to look and get a bit of both the above methods like the range method and the typical for loop access.

start, i, end := 2, 2, 5

modes := []string{"normal", "command", "insert", "visual", "select", "replace"}

for range scores[start:end] {
    fmt.Printf("Element at index %d = %s \n", i, modes[i])
    i++
}
$ go run slices.go
Element at index 2 = insert
Element at index 3 = visual
Element at index 4 = select

So by using the range keyword we were able to iterate over the slice but without assigning the iterator and the copy of the element, we manually set a counter i and increment it as per our liking. Using index slicing we were able to get the elements between particular indices.

That's it from this part. Reference for all the code examples and commands can be found in the 100 days of Golang GitHub repository.

Conclusion

So, from this part of the series, we were able to understand the basics of slices in golang. We covered some basics stuff including the declaration, initialization, and iteration. We also covered the under the hood working of slices and how to relate with the arrays. Thank you for reading. If you have any questions or feedback, please let me know in the comments or on social handles. Happy Coding :)