11 minutes read

Today we continue our discussion about slices. In this topic, we will learn how to copy slices, how to append elements to a slice, and how we can use the range keyword to iterate over a slice or an array.

For range loop with an array and a slice

To begin with, consider we have a slice.

var s = []int{0, 10, -3, 5, 99}

To iterate over a slice, we can write this:

for index := 0; index < len(s); index++ {
    // processing slice elements
}

Now let's introduce the for range loop:

for index := range s {
    // processing slice elements
}

These two examples above are logically equal. But as you can see, with the help of the range keyword, we end up writing less code, and it becomes clearer. Furthermore, when using this keyword, you can obtain a copy of a slice element alongside its slice index:

for index, element := range s {
    // processing slice elements
}

When you need only the copy of a slice element, use _ to indicate that you don't need the index. Otherwise, the Go compiler will throw you the declared but not used error because of the unused index variable.

for _, element := range s {
    // processing slice elements
}

It is important to understand that the assignment of a new value to a copied element will have no effect on the original one. Thus, the element you receive through the range keyword is for read-only purposes:

for _, element := range s {
    element = newValue   // this assignment will have no effect 
}                        // on the original slice element

If you want to change a slice element inside a loop, use its index:

for index := range s {
    s[index] = newValue
}

The copy function

In Go, we have a special built-in function used to copy slices – the copy function. Its first argument is the destination, and the second one is the source. Excluding one special case, this function works only with slices.

The special case we've mentioned is a string, and we'll discuss it in an upcoming topic.

In addition, the copy function returns the number of copied elements. It will be the minimum of len(source) and len(destination). Let's have a look at the following example:

var s = []int{12, 23, 34}
var sn = make([]int, len(s))

var n = copy(sn, s)  // var n is the number of copied elements 

sn[0] = 0            // Here, we assign a new value to the slice elements in order
sn[1] = 11           // to see whether it is a proper copy or the same slice

fmt.Println(n)       // 3
fmt.Println(s)       // [12 23 34] - the initial slice with no changes
fmt.Println(sn)      // [ 0 11 34] - the copied slice with modified elements

The copy function was built in a way that it copies only available elements to available places. It means that if you copy a slice of length 3 to a slice of length 0, the function won't copy anything:

var s = []int{12, 23, 34}
var sn []int

var n = copy(sn, s)

fmt.Println(n)  // 0
fmt.Println(sn) // []

If the number of copied elements is irrelevant, you can simply write:

copy(sn, s)

The append function

The key feature of a slice is its variable size. In Go, we use a built-in function append to extend slices by adding new elements to the end of one. The function's argument is a slice to which we want to add elements, followed by the elements themselves. The append function returns a new extended slice that we should save. Here is an illustration of appending elements to a slice:

var s = []int{12, 23, 34}
s = append(s, 45)      
fmt.Println(s) // [12 23 34 45]

s = append(s, 56, 67)
fmt.Println(s) // [12 23 34 45 56 67]

After its first argument, the append function can take any number of elements. We have a special syntax for breaking a slice into a series of function arguments:

var s1 = []int{12, 23, 34}
var s2 = []int{45, 56, 67}

var s = append(s1, s2...) 
// Like that, we can append one slice to another

These three dots after the s2 slice tell Go to break the s2 slice down to a series of arguments for the append function. It is somewhat like doing the following:

var s = append(s1, s2[0], s2[1], s2[2])

However, since the size of a slice can change after compilation, the three-dot notation is indispensable for appending one slice to another. What's more, the append function can add elements even to a nil slice, and initialize it:

var s []int       // s is []
s = append(s, 10) // s is [10]

Slice allocation and reallocation

In the example above, we first have a nil slice with len and cap equal to 0. Clearly, we have no room for any new elements. In this case, the append function will initialize a new backing array with a greater capacity and will copy the data from the old backing array to the new one.

It is good practice to manually initialize a slice of such a capacity that would minimize the need for reallocation.

We can establish the capacity we need from the logic of a task:

var a = []int{1, 2, 4, 3, 6}
var b = []int{-1, 9, -90}

// We want to join two slices, which means we already know
// the capacity of the new slice: len(a)+len(b)

var s = make([]int, 0, len(a)+len(b))
s = append(s, a...)
s = append(s, b...)

We can also assume how many elements we'll need. For instance, if we are working on some historical analysis of the world's countries, we can assume how many countries there were in the past and how many there are now and create a slice of countries with an appropriate capacity:

var countries = make([]string, 0, 2000) // len: 0 | cap: 2000

It is essential to understand that if we only pass the length without the capacity to the make function, it will return a slice with a capacity equal to its length. In such case, on calling the append function, and appending a new element, Go will reallocate the slice to increase its capacity:

var countries = make([]string, 2000) // len: 2000 | cap: 2000
countries = append(countries, "Indonesia")

fmt.Println("cap:", cap(countries))  // cap: 3072

On the other hand, if we assign values of the elements manually, using indexes within the slice range, no reallocation occurs:

var countries = make([]string, 2000) // len: 2000 | cap: 2000
for i := range countries {
    countries[i] = "Indonesia" // Here, we assign one and the same value to all 
                               // elements just to show you the slice behavior 
}

fmt.Println("cap:", cap(countries))  // cap: 2000

Conclusion

Today we have covered two important slice functions – copy and append, and have learned how to perform iterations over a slice or an array by using the for range keywords. Now let's move on to the exercises to ensure all this information stays with us!

78 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo