Using arrays and slices
Languages require more than the basic types to hold data. The array
type is one of the core building blocks in lower-level languages, providing the base sequential data type. For most day-to-day use, Go's slice
type provides a flexible array that can grow as data needs grow and can be sliced into sections in order to share views of the data.
In this section, we will talk about arrays as the building blocks of slices, the difference between the two, and how to utilize them in your code.
Arrays
The base sequential type in Go is the array (important to know, but rarely used). Arrays are statically sized (if you create one that holds 10 int
types, it will always hold exactly 10 int
types).
Go provides an array
type designated by putting [size]
before the type you wish to create an array of. For example, var x [5]int or x := [5]int{}
creates an array holding five integers, indexed from 0
to 4
.
An assignment into an array is as easy as choosing the index. x[0] = 3
assigns 3
to index 0
. Retrieving that value is as simple as referring to the index; fmt.Println(x[0] + 2)
will output 5
.
Arrays, unlike slices, are not pointer wrapper types. Passing an array as a function argument passes a copy:
func changeValueAtZeroIndex(array [2]int) { array[0] = 3 fmt.Println("inside: ", array[0]) // Will print 3 } func main() { x := [2]int{} changeValueAtZeroIndex(x) fmt.Println(x) // Will print 0 }
Arrays present the following two problems in Go:
- Arrays are typed by size –
[2]int
is distinct from[3]int
. You cannot use[3]int
where[2]int
is required. - Arrays are a set size. If you need more room, you must make a new array.
While it is important to know what arrays are, the most common sequential type used in Go is the slice.
Slices
The easiest way to understand a slice is to see it as a type that is built on top of arrays. A slice is a view into an array. Changing what you can see in your slice's view changes the underlying array's value. The most basic use of slices acts like arrays, with two exceptions:
- A slice is not statically sized.
- A slice can grow to accommodate new values.
A slice tracks its array, and when it needs more room, it will create a new array that can accommodate the new values and copies the values from the current array into the new array. This happens invisibly to the user.
Creating a slice can be done similarly to an array, var x = []int
or x := []int{}
. This creates a slice of integers with a length of 0
(which has no room to store values). You can retrieve the size of the slice using len(x)
.
We can create a slice with initial values easily: x := []int{8,4,5,6}
. Now, we have len(x) == 4
, indexed from 0
to 3
.
Similar to arrays, we can change a value at an index by simply referencing the index. x[2] = 12
will change the preceding slice to []int{8,4,12,6}
.
Unlike arrays, we can add a new value to the slice using the append
command. x = append(x, 2)
will cause the underlying x
array references to be copied to a new array and assigns the new view of the array back to x
. The new value is []int{8,4,12,6,2}
. You may append multiple values by just putting more comma-delimited values in append
(that is, x = append(x, 2, 3, 4, 5)
).
Remember that slices are simply views into a trackable array. We can create new limited views of the array. y := x[1:3]
creates a view (y
) of the array, yielding []int{4, 12}
(1
is inclusive and 3
is exclusive in [1:3]
). Changing the value at y[0]
will change x[1]
. Appending a single value to y
via y = append(y, 10)
will change x[3]
, yielding []int{8,4,12,10,2}
.
This kind of use isn't common (and is confusing), but the important part is to understand that slices are simply views into an array.
While slices are a pointer-wrapped type (values in a slice passed to a function that are changed will change in the caller as well), a slice's view will not change.
func doAppend(sl []int) { sl = append(sl, 100) fmt.Println("inside: ", sl) // inside: [1 2 3 100] } func main() { x := []int{1, 2, 3} doAppend(x) fmt.Println("outside: ", x) // outside: [1 2 3] }
In this example, the sl
and x
variables both use the same underlying array (which has changed in both), but the view for x
does not get updated in doAppend()
. To update x
to see the addition to the slice would require passing a pointer to the slice (pointers are covered in a future chapter) or returning the new slice as seen here:
func doAppend(sl []int) []int { return append(sl, 100) } func main() { x := []int{1, 2, 3} x = doAppend(x) fmt.Println("outside: ", x) // outside: [1 2 3 100] }
Now that you see how to create and add to a slice, let's look at how to extract the values.
Extracting all values
To extract values from a slice, we can use the older C-type for
loop or the more common for
...range
syntax.
The older C style is as follows:
for i := 0; i < len(someSlice); i++{ fmt.Printf("slice entry %d: %s\n", i, someSlice[i]) }
The more common approach in Go uses range
:
for index, val := range someSlice { fmt.Printf("slice entry %d: %s\n", index, val) }
With range
, we often want to use only the value, but not the index. In Go, you must use variables that are declared in a function, or the compiler will complain with the following:
index declared but not used
To only extract the values, we can use _,
(which tells the compiler not to store the output), as follows:
for _, val := range someSlice { fmt.Printf("slice entry: %s\n", val) }
On very rare occasions, you may want to only print out indexes and not values. This is uncommon because it will simply count from zero to the number of items. However, this can be achieved by simply removing val
from the for
statement: for index := range someSlice
.
In this section, you have discovered what arrays are, how to create them, and how they relate to slices. In addition, you've acquired the skills to create slices, add data to slices, and extract data from slices. Let's move on to learning about maps next.
Understanding maps
Maps are a collection of key-value pairs that a user can use to store some data and retrieve it with a key. In some languages, these are called dictionaries (Python) or hashes (Perl). In contrast to an array/slice, finding an entry in a map requires a single lookup versus iterating over the entire slice comparing values. With a large set of items, this can give you significant time savings.
Declaring a map
There are several ways to declare a map. Let's first look at using make
:
var counters = make(map[string]int, 10)
The example just shared creates a map with string
keys and stores data that is an int
type. 10
signifies that we want to pre-size for 10 entries. The map can grow beyond 10 entries and the 10
can be omitted.
Another way of declaring a map is by using a composite literal:
modelToMake := map[string]string{ "prius": "toyota", "chevelle": "chevy", }
This creates a map with string
keys and stores the string
data. We also pre-populate the entry with two key-value entries. You can omit the entries to have an empty map.
Accessing values
You can retrieve a value as follows:
carMake := modelToMake["chevelle"] fmt.Println(carMake) // Prints "chevy"
This assigns the chevy
value to carMake
.
But what happens if the key isn't in the map? In that case, we will receive the zero value of the data type:
carMake := modelToMake["outback"] fmt.Println(carMake)
The preceding code will print an empty string, which is the zero value of the string type that is used as values in our map.
We can also detect if the value is in the map:
if carMake, ok := modelToMake["outback"]; ok { fmt.Printf("car model \"outback\" has make %q", carMake) }else{ fmt.Printf("car model \"outback\" has an unknown make") }
Here we assign two values. The first (carMake
) is the data stored in the key (or zero value if not set), and the second (ok
) is a Boolean that indicates if the key was found.
Adding new values
Adding a new key-value pair or updating a key's value, is done the same way:
modelToMake["outback"] = "subaru" counters["pageHits"] = 10
Now that we can change a key-value pair, let's look at extracting values from a map.
Extracting all values
To extract values from a map, we can use the for
...range
syntax that we used for slices. There are a few key differences with maps:
- Instead of an index, you will get the map's key.
- Maps have a non-deterministic order.
Non-deterministic order means that iterating over the data will return the same data but not in the same order.
Let's print out all the values in our carMake
map:
for key, val := range modelToMake { fmt.Printf("car model %q has make %q\n", key, val) }
This will yield the following, but maybe not in the same order:
car model "prius" has make "toyota" car model "chevelle" has make "chevy" car model "outback" has make "subaru"
Note
Similar to a slice, if you don't need the key, you may use _
instead. If you simply want the keys, you can omit the value val
variable, such as for key := range modelToMake
.
In this section, you have learned about the map
type, how to declare them, add values to them, and finally how to extract values from them. Let's dive into learning about pointers.