About arrays and slices in Go

Arrays in Go were one of the toughest topics for me because I didn’t understand how they work. In this article, we will look at exactly how slices and arrays work in Go, as well as exactly how append And copy.

Arrays

Arrays – a collection of elements of the same type. Array length Not may change. Here is how we can create an array in Go:

arr := [4]int{3,2,5,4}

If we create two arrays in Go with different lengths, then the two arrays will have different types, because the length of an array in Go is part of its type:

a := [3]int{}
b := [2]int{}

// (a) [2]int и (b) [3]int - разные типы

Moreover, if we are too lazy to write the length of the array, then we can tell the compiler to calculate the length itself:

a := [...]int{1, 2, 3} // [3]int

Pass by value

The variable that we initialized with the array value contains exactly the values ​​of the array, and not a reference to the first element of the array (as is done in C).

That is why an array in Go is a primitive data type, it can be copied when passed to another variable. By default in Go, all values ​​are copied rather than passed by reference. This means that if we pass our array to a function, then Go will copy this array and the function will already contain a completely different array (or rather, an exact copy of the original array).

Below we’ll look at an example where we’ll copy an array and then look at the address where the value is stored:

package main

import "fmt"

func main() {
	var initArray = [...]int{1, 2, 3}
	var copyArray = initArray

	fmt.Printf("Address of initArray: %p\n", &initArray)
	fmt.Printf("Address of copyArray: %p\n", &copyArray)
}

/*
Output:
  Address of initArray: 0xc00001a018
  Address of copyArray: 0xc00001a030
*/

slices

Slices in Go are more flexible, they allow you to change their length. Slices are essentially a superset of arrays. Slices create an array for us, which we can use as a regular array and, if necessary, expand it.

Slices can be created in two ways:

// С помощью make
var foo []byte
s = make([]byte, 5, 5)

// С помощью shorthand syntax
bar := []byte{}

The make method

Method with make is more interesting because it gives us the ability to set the type, length, and capacity.

With the type, I think there should be no problems. The slice type is formed in the form []тип.

With a long, too, nothing interesting. Depending on the amount entered, the array will be filled with zero values, for example:

package main

import "fmt"

func main() {
	var foo = make([]byte, 5)
	var bar = make([]int, 10)
	var fee = make([]string, 2)

	fmt.Println(foo, bar, fee)
}

/*
Output:
  [0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [ ]
*/

The last parameter is capacity plays an important role in program performance and is also interesting. In fact, it tells about how much memory we need to allocate in advance for our array, so that when expanding, we do not have to look for a new piece of memory.

For example, if we create an array with a capacity of 10 elements, fill it with 5 elements, and then add one, the address of the array will not change:

package main

import "fmt"

func main() {
	var foo = make([]int, 5, 10)
	fmt.Printf("Address of foo array [before append]: %p\n", &foo)

	foo = append(foo, 222)
	fmt.Printf("Address of foo array [after append]: %p\n", &foo)
}

/*
Output:
	Address of foo array [before append]: 0xc0000aa018
	Address of foo array [after append]: 0xc0000aa018
*/

By the way, if we did not explicitly set the slice capacity (that is, we used the construction make([]int, 5)), then the capacity will be equal to the length of the array (in this case, 5).

If we specify the array capacity less than its length, then the code will not compile at all:

package main

import "fmt"

func main() {
	var foo = make([]int, 5, 4)
	fmt.Printf("Capacity of the array: ", cap(foo))
}

/*
	./prog.go:6:24: invalid argument: length and capacity swapped
*/

What happens if we overfill the capacity?

If we overflow the slice capacity, then the capacity will be multiplied by 2:

package main

import "fmt"

func main() {
	var foo = make([]int, 10, 10) // Изначальная вместимость - 10
	foo = append(foo, 2) // Добавляем элемент
	fmt.Println("Length of the array:", len(foo))
	fmt.Println("Capacity of the array:", cap(foo))
}

/*
Output:
	Length of the array: 11
	Capacity of the array: 20
*/

In this case, the following will happen in memory:

  1. Go understands that we do not have enough memory and will see if there are the same number of cells after the current memory segment;

  2. If there are cells, it will not move the array and will just reserve more memory;

  3. If there are no cells, then it will copy all the information from the already used segment and find twice as many free cells, after finding it, it will transfer all the data there and give us the segment address;

shorthand-syntax

With the short version of the slice declaration, things are simpler:

package main

import "fmt"

func main() {
	foo := []int{1, 2, 3}
	fmt.Println("Length of the array:", len(foo))
	fmt.Println("Capacity of the array:", cap(foo))
}

/*
Output:
	Length of the array: 3
	Capacity of the array: 3
*/

In the example above, Go will create an array (under the hood) with a length of three cells and the same capacity.

Slices on slices

A slice on a slice is a child slice that refers to only part of the slice:

package main

import "fmt"

func main() {
	name := []string{"D", "a", "n", "i", "i", "l"}
	firstThreeLetters := name[:3]
	fmt.Println(firstThreeLetters)
}

/*
Output:
	[D a n]
*/

Although slice And slice – the concepts are interchangeable (to be more precise, a slice is a translation from the English slice), we will call all newly created slices slices using make () or shorthand syntax, and slices will be called slices made over an existing array.

We can also slice arrays, so we can make arrays dynamically expandable:

package main

import "fmt"

func main() {
	nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
	nameSlice := nameArray[:]
	nameSlice = append(nameSlice, "!")
	fmt.Println(nameSlice)
}

/*
Output:
	[D a n i i l !]
*/

Slice under the hood

The slice under the hood is a structure that contains a reference to the original array, length and capacity:

struct {
	array *[]T
	length int
	capacity int
}

When we create a new slice or slice an array, the array reference is assigned to the field arraywith the help of this pointer, the slice will be able to access the array under the hood. length And capacity store length and capacity, respectively.

Because the slice refers to part of the array, we can cut off part of the array. A slice does not copy the elements of an array, it simply refers to them. Thus, when the slice changes, the array from which we took the slice will also change:

package main

import "fmt"

func main() {
	nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
	nameSlice := nameArray[:3]
	nameSlice[len(nameSlice) - 1] = "m"
	fmt.Println(nameSlice) // [D a m]
	fmt.Println(nameArray) // [D a m i i l]
}

We can also make the slice span the entire length of the original array. Since the slice stores the capacity of the original array – we can make the slice again and specify the parameter cap(nameArray):

package main

import "fmt"

func main() {
	nameArray := [6]string{"D", "a", "n", "i", "i", "l"}
	nameSlice := nameArray[:3]
	nameSlice[len(nameSlice)-1] = "m"
	fmt.Println(nameSlice) // [D a m]

	// Делаем новый срез
	nameSlice = nameSlice[0:cap(nameSlice)]
	fmt.Println(nameSlice) // [D a m i i l]
}

You may be wondering why we didn’t cut cap(nameSlice) - 1, because we specified a non-existent index at the end (one more than exists in the array). The thing is that the last element in the slice is not included in the slice.

That is, the first index is inclusive in the slice, and the last index is non-inclusive.

copying

As you can already understand, when slicing from an array or slice, we do not create a new slice. Also, if we assign a slice value to one variable to another variable, they will both point to the same array:

package main

import (
	"fmt"
)

func main() {
	nameSlice := []string{"D", "a", "n", "i", "i", "l"}
	secondNameSlice := nameSlice
	secondNameSlice[0] = "T"
	fmt.Println(nameSlice, secondNameSlice) // [T a n i i l] [T a n i i l]
}

We can avoid this behavior by using copy. In order to copy a slice (create an independent copy) – we just need to use the function copy:

package main

import (
	"fmt"
)

func main() {
	nameSlice := []string{"D", "a", "n", "i", "i", "l"}
	secondNameSlice := make([]string, len(nameSlice), cap(nameSlice))
	copy(secondNameSlice, nameSlice)
	secondNameSlice[0] = "T"

	fmt.Println(nameSlice, secondNameSlice) // [D a n i i l] [T a n i i l]
}

copy and append under the hood

We can notice two differences: when using the function append – we reassigned the value of the variable:

foo := []int {}
foo = append(foo, 1)

In case of copy we just pass the variable itself (not a reference, but a variable!):

foo := []int {1, 2}
bar := []int {}
copy(bar, foo)

Here’s how copy works under the hood:

func copy(to []T, from []T) {
	for i := range from {
		to[i] = from[i]
	}
}

The Go developers decided not to add the new slice initialization part inside copy.

In the case of the add-on, everything is a little different. Here, the Go developers considered that the function itself should decide whether it is necessary to initialize a new slice, or whether data can be added to an existing slice:

func append(slice []T, data ...T) []T {
    initialLength := len(slice)
    finalLength := m + len(data)
    if finalLength > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:finalLength]
    copy(slice[initialLength:finalLength], data)
    return slice
}

Instead of a conclusion

If you liked this article, then you can always go to my blogthere is more related information about web development.

If you have any questions – feel free to ask them in the comments. Have a good time! 💁🏻‍♂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *