Golang composite data types – Arrays and Slices

Go supports composite types such as array, slice, maps and structures. Composite types are made up of other types – built-in types and user-defined types. In this blog we will deal with arrays and slices.

golang arrays and slices

Golang Arrays

An array is collection of elements of same data type, for instance an array can be collection of integers with elements like -1, 31, 45, 52 being part of it. Note: An array that contains elements from different data types in not allowed in Golang.

Declaring arrays

An array is typically declared with length and type and is represented as [n]T, where n is the length of the array (number of elements in the array) and T is data type of these elements.

In the below code snippet, var names [3] string declares an array with variable names, which has 3 elements of string data type. By default this array will be empty [ ] which is the zero value of string data type. We can now populate this array by assigning each indice names[0], names[1], names[2] with appropriate strings. We can access the elements of an array individually using indices as well, similar to other programming languages.

We can also use shorthand to create an array by assigning the elements during declaration. In the below code snippet, we created array like this: mynames : = [3]string{“Alice”, “Bob”, “Celine”}

Shorthand declaration can be further optimized to use ellipses […], so we can create arrays like scores := [3]int{12, 78, 50} where the compiler calculates and sets the length of the array.


package main
import (
"fmt"
"reflect"
)
func main() {
/* Declaring an array and adding elements */
var names [3]string //string array with length 3
names[0] = "Alice" // array index starts at 0
names[1] = "Bob"
names[2] = "Celine"
/* Shorthand declaration */
mynames := [3]string{"Alice", "Bob", "Celine"}
fmt.Println(names, mynames)
/* Compiler determines the length of array */
scores := []int{12, 78, 50}
fmt.Println(scores, len(scores))
/* The size of the array is a part of the type – [3]string */
fmt.Println(mynames, reflect.TypeOf(mynames)) /* Prints: [Alice Bob Celine] [3]string*/
}

We mentioned earlier that an array is represented as [n]T, but actually, in Golang array belongs to type n[T] where size of the array is also the part of the data type. The size is even part of the definition of an array, so the two arrays [10]int and [20]int are not just two int arrays of different size but are in fact are of two different types. Sounds weird, but can we check that? Let’s use the “reflect” package of Golang to do it. Say we declare the array as mynames : = [3]string{“Alice”, “Bob”, “Celine”}, but we can get the data type of the array with reflect.TypeOf(mynames) – this will return [3]string. Note how [3] is also part of the data type.

It’s also convenient to calculate the length of array using an in built function len(array), the code example above manifests the usage of len() function to calculate the length of array scores.

Arrays are value types

You must have heard about value types and reference types in multiple languages, like in C – we have pass by value and pass by reference. In Go, arrays are value types. So if an array is copied and the copied array is changed, then a change in the new array does not change the original array. So the array countries will still return [USA UK AU] while mycountries will return [USA NZ AU] in the code example.


package main
import (
"fmt"
)
func changeArray(numbers [5]int) {
numbers[0] = 55 /* Passed by value */
fmt.Println("Changed numbers ::", numbers)
}
func main() {
/*Arrays are value types*/
countries := []string{"USA", "UK", "AU"}
mycountries := countries
mycountries[1] = "NZ" /* Change in mycountries does not change countries */
fmt.Println(countries, mycountries) /* Prints: [USA UK AU] [USA NZ AU]*/
numbers := []int{1, 2, 4, 5, 8}
changeArray(numbers) /* Prints: Changed numbers :: [55 2 4 5 8]*/
fmt.Println("Original numbers:", numbers) /* Original array is unchanged, Prints: Original numbers: [1 2 4 5 8] */
}

Similarly when arrays are passed to functions as parameters, they are passed by value and the original array is unchanged. In the above code snippet, there is no impact of changeArray function on the original array numbers.

Array operations

Great, so we now understand how to declare arrays and access individual elements. Let’s go ahead and do some useful operations on arrays.

Iterating over array elements can be done via a for loop. In the below code, we use for loop to go through the array elements using indices. Go also provides a convenient method range which returns the index and the value at the index. So while iterating through the array you can both print the index and the value. You could also ignore the index value with underscore ‘_’ (demonstrated in the code).


package main
import (
"fmt"
)
func main() {
/* Short hand declaration */
mynames := [3]string{"Alice", "Bob", "Celine"}
/* Iterate through array using indices */
for i := 0; i < len(mynames); i++ {
fmt.Printf("%s", mynames[i])
}
/* Using range function */
for i, v := range mynames {
fmt.Println(i, v)
}
/* Ignoring the index with _ */
for _, v := range mynames {
fmt.Println(v)
}
}

Array types are also comparable, so we can compare two arrays of that type using the == operator, which reports whether all corresponding elements are equal and != operator is its negation. It is important to note that array comparison takes into the account the data type (including the length) and the contents of the array, which is evident in the example.


package main
import (
"fmt"
)
func main() {
/* Shorthand declaration */
scores := [4]int{80, 85, 45, 55}
herScores := []int{80, 85, 45, 55}
hisScores := [4]int{80, 45, 22, 33}
fmt.Println(scores == herScores) /* Returns truee */
fmt.Println(scores == hisScores) /* Returns false */
fmt.Println(herScores == hisScores) /* Returns false */
}

Cool, so we now know a lot about arrays; while they are great, but because they are of fixed size, we cant do much with them. Let’s look at an alternative composite data type that might help. Slices…incoming…

Golang Slices

Concept of Slices is Golang is very thoughtful and clever. In fact, once you start programming in Golang, you will see that you often use slices over arrays. Slices in Golang are just a view or a window on top of an array and allow us to create a variable length data types. Slices are like a dynamic layer on top of arrays, so creating a slice from an array neither allocates new memory nor copies anything. They are just references to existing arrays.

Slices are represented as []T where [] indicates that slices are not of fixed length. A slice has three components:

  • Pointer: it points to the first element of the array that is reachable through the slice and is not necessarily the array’s first element.
  • Length: the length is the number of slice elements. (Note: it can’t exceed the capacity).
  • Capacity: usually the number of elements between the start of the slice and the end of the underlying array.

Creating Slices

Slices can be created in two ways using a shorthand notation like slices := []string{“AB”, “CD”, “EF”} or from an existing array var slice []int = array[1:4]. Below code snippet shows both the styles of creating slices. Note, when we say array[1:4] the slice is created with values of array on indices 1, 2 and 3.


package main
import "fmt"
func main() {
slices := []string{"AB", "CD", "EF"}
fmt.Println(slices)
array := [5]int{76, 77, 78, 79, 80}
var slice []int = array[1:4] /*creates a slice from a[1] to a[3] */
/* Returns [77, 78, 79] */
fmt.Println(slice)
vegies := []string{"Potato", "Tomato", "Eggplant", "Onion", "Capsicum"}
vegslice := vegies[1:3]
fmt.Printf("Length of slice %d capacity %d", len(vegslice), cap(vegslice))
/* Returns Length of slice 2 capacity 4 */
myslice := make([]int, 3, 3)
fmt.Println(myslice) /* Returns [0 0 0] */
}

The length of the slice is the number of elements in the slice. The capacity of the slice is the number of elements in the underlying array starting from the index from which the slice is created. (We will also see how the capacity of the slice can be increased.) In the code example, though the length of the slice vegslice is 2, it’s capacity is that of the underlying array vegies which is 4. A slice can be re-sliced up to its capacity, beyond which the program will throw a run time error.

Slices can also be created using the make method by passing the length and the capacity, for example, make([]int, 3, 3). When use make to create a slice, the slice is initialized with zero values as we will see in the code example.

Slice operations

Lets start with the most important operation on slices: appending elements. append() adds elements to the end of the slice, thus increasing the length of the size. The definition of append function is func append(s []T, x …T) []T.x …T indicates that slices canaccept any number of parameters for argument. In Go, these functions are called variadic functions. As in the code example below, we can add elements to a slice like this: vegslice = append(vegslice, “Okra”, “Cabbage”)

Zero value of slice is nil. append() can add elements to a nil slice making the slice meaningful. In the code snippet, we declare a nil slice names and add strings to it using append(), thus increasing the length and capacity.

Also if we add more elements to the slice than its capacity, append() automatically takes care of allocating a new array and copying the old content over. The elements of the existing array are copied to this new array and a new slice reference for this new array is returned.

So as you can see append() can either change the existing array on which it is based or it can create a new array altogether. This can be confusing at times to the programmers, if the older array was referenced earlier, the variables referencing the array may get stale data.


package main
import "fmt"
func main() {
var names []string /* Zero value of a slice is nil */
fmt.Println(names, len(names), cap(names)) /* Returns [] 0 0 */
names = append(names, "John", "Bill", "Steve")
fmt.Println(names, len(names), cap(names)) /* Returns [John Bill Steve] 3 3 */
vegies := []string{"Potato", "Tomato", "Eggplant", "Onion", "Capsicum"}
vegslice := vegies[1:3]
/* Returns [Tomato Eggplant] 2 4 */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
vegslice = append(vegslice, "Okra", "Cabbage")
/* Returns [Tomato Eggplant Okra Cabbage] 4 4 */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
vegslice = append(vegslice, "Lettuce", "Bottlegaurd")
/* Returns [Tomato Eggplant Okra Cabbage Lettuce Bottlegaurd] 6 8 */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
}

In the above code example, initially the length of slice is 2 and capacity is 4, so we could add more veggies (Okra and Cabbage) to it to make the length 4 while the capacity remained 4. Later when we added Lettuce and Bottleguard, the length and capacity both increased to 6 and 8 respectively. This operation will create a new array and copy the old contents of the array into the new one.

But how do we know if the underlying array has really changed and a new one is created? We take help of reflect.SliceHeader() method that contains a Data field which contains a pointer to the underlying array of a slice. Let’s instrument the above code to get the pointer of the underlying array of the slice.


package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
vegies := []string{"Potato", "Tomato", "Eggplant", "Onion", "Capsicum"}
vegslice := vegies[1:3]
/* Returns
[Tomato Eggplant] 2 4
Pointer to the underlying array 824634236944 */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
hdr0 := (*reflect.SliceHeader)(unsafe.Pointer(&vegslice))
fmt.Println("Pointer to the underlying array", hdr0.Data)
vegslice = append(vegslice, "Okra", "Cabbage")
/* Returns
[Tomato Eggplant Okra Cabbage] 4 4
Pointer to the underlying array 824634236944 */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&vegslice))
fmt.Println("Pointer to the underlying array", hdr.Data)
vegslice = append(vegslice, "Lettuce", "Bottlegaurd")
/* Returns [Tomato Eggplant Okra Cabbage Lettuce Bottlegaurd] 6 8
Pointer to the underlying array 824634245120 << Changed */
fmt.Println(vegslice, len(vegslice), cap(vegslice))
hdr2 := (*reflect.SliceHeader)(unsafe.Pointer(&vegslice))
fmt.Println("Pointer to the underlying array", hdr2.Data)
}

In the code snippet, when we add Okra and Cabbage the capacity of the slice remains same and the underlying array is also same (pointer location 824634236944), but when we add Lettuce and Bottleguard, a new underlying array gets created (pointer location 824634245120) and the capacity of slice also increased to 8.

Iterating over slice

Like we saw with arrays, we can iterate over elements in a slice with a for loop. Again, the range method can be used here as well to go through all the elements. Individual elements in the slice can be accessed with indices. You can view the usage of indices and for loop in the below code example.

Passing slices to functions

As we learnt previously, arrays are considered as value types, even slices are passed as values to functions but there is a slight difference, actually they are passed as references.

A slice can be represented as a structure with length, capacity and a pointer to the zeroth element of the underlying array. We will learn more about structures and pointers soon, for now you can think of structures and pointers like you know them in C language.

/* Slice as a structure */
type slice struct {       
      Length        int     
      Capacity      int     
      ZeroElement   *byte 
}

As we pass slice (actually structure) to the function, the pointer will still refer to the same underlying array, so any changes to the array (through the slice) done in the function are available even after the function scope.


package main
import (
"fmt"
)
/* Function that doubles every element in the slice */
func workonslice(slice []int) {
for i := range slice {
slice[i] *= 2
}
}
func main() {
/* Create slice from array of integers */
numbers := []int{1, 2, 3, 4, 5}
slice := numbers[1:4]
/* Returns: Elements in slice are: [2 3 4] */
fmt.Println("Elements in slice are:", slice)
/* Get 1st element in slice through indices */
/* Returns: First elements in slice: 2 */
fmt.Println("First elements in slice:", slice[0])
/* Function call on slice */
workonslice(slice)
/* Returns: Elements in slice post function call: [4 6 8] */
fmt.Println("Elements in slice post function call:", slice)
}

Copying slices

But what if we do not want the original slice changed? We could create a copy of the original slice into a new slice, this will ensure the original contents are stored and remain unchanged. Copy function is represented as copy(dst, src []T)int , so the elements of src slice are copied in dst slice and the value returned is the number of elements copied.

/* Copy elements of slice in to a new one */

mySlice := make([]int, len(slice))
val := copy(mySlice, slice)

Copying of slices also helps in garbage collection, thus optimizing memory. Slices hold a reference to the underlying array. As long as the slice is in memory, the array cannot be garbage collected. So if we copy the slice and work on it, the original array can get garbage collected.

Good Reads on Arrays and Slices:

Cool, I’m sure you are now confident with arrays and slices in Golang. Interesting stuff ahead, stay tuned.. 😁

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.