Hey, another week and another Go tutorial! Hope you are doing OK in these strange times?!
This week we’re covering more Go composite types; we’re looking at arrays, slices and maps. We’re also going to discuss pointers and pointer semantics.
We had a brief look at pointers in the last tutorial on structs, and TBH, that wasn’t a bad start, but, since pointers (or references) will be relevant to much of what we cover in this tutorial we’ll start here.
Pointers vs Values
If you are coming to Go from dynamically typed, interpreted languages, the concepts discussed here may seem alien at first, but they are in fact very simple.
Let’s dive straight in.
So what is a pointer? Well, it is a variable, often referred to as a reference, that holds an address of a part of memory, where another value is stored. It ‘points’ to that other value in theory. In practice, it holds the address where that other value is to be found in memory.
Let’s look at a really simple example that illustrates the concepts perfectly.
myValue := "Golang At Speed"
fmt.Printf("myValue has a value of `%v`\n", myValue)
fmt.Printf("myValue is in memory address %v\n\n", &myValue)
myPointer := &myValue
fmt.Printf("myPointer is a reference, it points to memory address %v\n\n", myPointer)
*myPointer = "What happened to Golang At Speed"
// note we are printing myValue again!
fmt.Printf("myValue has a value of `%v`\n", myValue)
I’ve reproduced this example on the Go playground since you really need to run this to see what is happening for yourself.
You can find it here: https://play.golang.org/p/4_zDwkD_Kw0.
Now, let’s walk through the code and explain the two alien-looking pieces of syntax, the “&” and the “*” symbols.
First, we create a string variable, though the type chosen is irrelevant. We print the value, and then we print the memory address in which that value is stored.
When we print the address, note how we tell the Go compiler that we want the address or reference of the value, and not the value itself using the ampersand symbol “&”.
Next, we have another variable, myPointer and we store the memory address of myValue, in the same way that we printed it - we make the assignment with the ampersand prefix to get the reference.
We print the contents of myPointer and we can see the memory address is the same as printed before. It holds the address of myValue.
Next, the exciting bit, we modify the original value indirectly, using its memory address!
To do that we need to tell the compiler that we want to use the value itself and not the reference we have in myPointer.
This is called “de-referencing” and to do this we use the asterisk prefix “*”.
The Go compiler now knows that we want to work with the value stored at the memory address, rather than the memory address stored in myPointer.
Finally, we print myValue again, and, as if by magic, its value has been changed!
You now understand Go’s pointer and value semantics!
Before we move on and see how this is applied when discussing arrays, slices and maps, I want to state that a pointer is not an alias of another variable.
It is itself a value, stored in its own memory address. Let’s look at one last example to illustrate:
myValue := "Golang At Speed"
fmt.Printf("myValue is in memory address %v\n\n", &myValue)
myPointer := &myValue
fmt.Printf("myPointer is a reference, it points to memory address %v\n\n", myPointer)
fmt.Printf("The memory address of myPointer %v\n\n", &myPointer)
Try it in the Go Playground here: https://play.golang.org/p/P41m2wN4ZT1.
See how the memory address of myPointer itself is not the same as the memory address of myValue. They are distinct. The memory address used for myPointer simply stores a value of the memory address used to store myValue.
Arrays
Arrays contain a list of elements. In Go, this list often, though not always, comprises elements of the same type.
In the below examples we have an array declaration. Size or capacity of the array is denoted by the number in the square brackets, with the type the array should store following it.
Arrays in Go, as in many languages are zero-bound. This means that an array of say size 10, will be accessed, for the purpose of setting and getting elements, using the indices 0 to 9. There is no index 10.
In our example below, the array has a length of 5 elements and a capacity of 5 elements. The array holds boolean values and elements are initialised to their nil values, which for the boolean type is false.
var myArray [5]bool
fmt.Printf("len:%v, cap:%v\n", len(myArray), cap(myArray))
fmt.Printf("element at index 0:%v\n", myArray[0])
myArray[1]=true
fmt.Printf("element at index 1:%v\n", myArray[1])
// errors, there is no index 5
myArray[5]=true
You can try the example here: https://play.golang.org/p/2wvJ9ttUe59.
In Go, arrays are fixed in length & capacity. And this actually forms part of their type definition, so they cannot be resized to hold more values.
Due to this inflexibility, unless we know at compile-time how many elements we will need to hold, and that this number will not increase at run-time, we will usually choose to work with the slice type instead.
Slices
A slice points to an array. Sometimes referred to as a reference type, a slice allows us to work with elements in an array, adding more if we need, without too much concern for the size of the backing array itself.
Should we want to add elements to a slice, and that takes the number of elements beyond the capacity of the existing backing array, the Go run-time will create a new backing array with additional capacity and copy over the existing values to the new array behind the scenes.
Below we have a similar example to that used previously, but instead of an array we declare a slice of type boolean.
Note that we leave the square brackets empty i.e. we set no length or size. This is how we declare a slice.
var mySlice []bool
fmt.Printf("len:%v, cap:%v\n", len(mySlice), cap(mySlice))
// errors
//fmt.Printf("element at index 0:%v\n", mySlice[0])
If we try to access an element we will get an error since we have zero-length at present. There are no elements in the backing array.
We can initialise both length and capacity when we create a slice using the make keyword as shown in the below examples.
// Slice with no length and a capacity of 5
mySlice := make([]bool, 0, 5)
// Slice with length of 4 and capacity of 5
// elements initialised to nil value of type
mySlice2 := make([]bool, 4, 5)
// Slice with length and capacity of 5 (alternative notation)
mySlice3 := make([]bool, 5)
To add elements to a slice we use the append keyword. In the background the Go run-time itself will manage the backing array, creating a new array with additional capacity if and when we need it i.e. when the length will exceed capacity.
Normally we do not need to care too much about what additional capacity Go will create, but, if your application is optimised for memory, you may wish to consider it.
Why? Because every time Go creates additional capacity by creating a new backing array, memory is allocated to provide this capacity.
For example, here we have a slice with a backing array of length 5 and capacity 5, when we add one additional value, Go needs to create further capacity since there is no room.
Arrays cannot be resized, so a new backing array is created with additional capacity and values copied over.
mySlice := make([]bool, 5)
mySlice = append(mySlice, true)
fmt.Printf("len:%v, cap:%v\n", len(mySlice), cap(mySlice))
After append, we find the capacity of the slice has grown to 16 despite us appending only 1 new element. Note now though, that the Go run-time will not need to recreate the backing array again and make further memory allocations until our slice length exceeds 16 elements.
The general rule, as with many things in Go, is not to concern yourself until a problem with the approach is evident.
However, if we know ahead of time what capacity we will need it can make sense to create this capacity, rather than have Go continually resize the backing array, possibly leaving us with more capacity than we needed and more memory allocated than is required.
Another point, and a common gotcha, if we allocate length as well as capacity with make, as we do above, then append creates additional length; the first 5 elements are used - although initialised to their nil values, which in this case, is false.
To assign new values to these existing nil elements, we do it in the same way as we do with arrays, by using the index of the element.
Finally, we should mention that we can create a slice and initialise values with assignment using a slice literal. I’m not going to labour this - we covered literals in depth when we covered structs - but suffice to say a slice literal of boolean values can be created as in the example below.
mySlice := []bool{true, true, true, true, true}
fmt.Printf("len:%v, cap:%v\n", len(mySlice), cap(mySlice))
By setting values for ourselves, the initial length and capacity are implicitly set for us.
Below, our final slice example demonstrates how we do not need to deal with pointer semantics when we are using slices, since a slice is already a pointer or reference to its backing array.
// makeTrue changes the element in the supplied slice
// to true, it does not need to accept a pointer, since a slice
// is itself a reference to an underlying backing array
func makeTrue(target []bool, element int) {
target[element] = true
}
func main() {
// slice of bool, length 5, capacity 5
mySlice := make([]bool, 5)
fmt.Println(mySlice)
// set the first element to true
mySlice[0] = true
fmt.Println(mySlice)
// pass to makeTrue, change the second element
makeTrue(mySlice, 1)
fmt.Println(mySlice)
}
The example is on the Go Playground here: https://play.golang.org/p/nVMVTdkSV11.
The absolute last thing we need to cover on slices relates to a common gotcha where we have an empty slice versus a nil slice.
The two things are not the same and mistaking them for such can lead to confusing behaviour in our programs.
See this example which demonstrates how similar looking slices can be empty versus nil.
package main
import (
"fmt"
)
func giveMeNilSlice()[]bool {
var mySlice []bool
return mySlice
}
func giveMeEmptySlice()[]bool {
// using slice literal
mySlice := []bool{}
return mySlice
}
func makeMeEmptySlice() []bool {
// using make
return make([]bool, 0, 0)
}
func main() {
if giveMeNilSlice() == nil {
fmt.Println("this is nil")
}
if giveMeEmptySlice() != nil {
fmt.Println("this is empty")
}
if makeMeEmptySlice() != nil {
fmt.Println("this is also empty")
}
}
You can try the example on the Go Playground: https://play.golang.org/p/77iksaEukUX.
Maps
Maps are the composite type used for storing associative data - values against keys.
In other languages, they may be called dictionaries or hashmaps. In Go, we simply call them maps.
The syntax for a map with keys of type string and values of type string looks like this:
var myStringStringMap map[string]string
As with slices, maps are often referred to as a reference type, which means we do not need to pass them around using their underlying memory address, in order to change the original. The reference is implicit.
Passing a map to a function which operates on the map, by say, modifying a map key’s value, operates on the original map - and not a copy.
We’ll get to the example of this in a moment, but first, we should highlight two common gotchas when working with maps.
First, a map must be initialised with the make keyword before we can use it, or as a map literal to give us an empty map. A nil map is of no use to us.
Let’s look at a few examples.
var myNilMap map[string]string
// we cannot assign to a nil map, this will not compile
myNilMap["mykey"] = "myvalue"
Try it here: https://play.golang.org/p/LmrbYWnNrpa. You will see an error, the compiler points out that you are trying to work with a nil map.
In the next two examples, we initialise the maps using make.
// this syntax is fine, map starts as a nil map
// use make to initialise the map to empty
var myNilMap map[string]string
myNilMap = make(map[string]string)
myNilMap["mykey"] = "my value"
// but this shortform, amounts to the same thing
emptyMap := make(map[string]string)
emptyMap["mykey"] = "another value"
fmt.Printf("myNilMap['mykey'] = '%v'\n", myNilMap["mykey"])
fmt.Printf("emptylMap['mykey'] = '%v'\n", emptyMap["mykey"])
You can try it here: https://play.golang.org/p/eZLkfTG-rRD.
In the following two examples, we create an empty map and then a second map which is initialised to non-nil values, using map literals. We also demonstrate how the map can be mutated, simply by passing it in as a value - we do not need its memory address, since it is already a reference to the key and value data it holds.
package main
import "fmt"
func alterMap(tar map[string]string, key string, newV string) {
tar[key] = newV
}
func main() {
emptyMap := map[string]string{}
initMap := map[string]string{
"key1": "value1",
"key2": "value2",
}
fmt.Printf("%+v\n", emptyMap)
fmt.Printf("%+v\n", initMap)
alterMap(initMap, "key1", "set to a new value")
fmt.Printf("%+v\n", initMap)
}
The example is here to experiment with: https://play.golang.org/p/BbExK7iSTlP.
The second gotcha concerns the fact that you cannot count on the contents of the map being ranged over in a particular order, not even in the order in which they were initially added to the map.
Have a look at this example.
package main
import (
"fmt"
)
func main() {
myMap := map[int]int{
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
10: 10,
}
for _,v := range myMap {
fmt.Println(v)
}
}
You can try running this example on the Go Playground a few times: https://play.golang.org/p/9KfZC2GfMaI. See how the printed order varies?
The last two topics we should cover are how to check if we have a key in a map, and how we should remove a key from a map. Both tasks are common operations when working with maps in Go.
Checking if a key exists in a map
We can check if a key exists in a map in the following manner. If it exists we get both the value and true as return values, and if it does not exist the test returns nil and false.
myMap := map[string]string{
"key1": "val1",
"key2": "val2",
"key3": "val3",
}
// test if exist ok == true
if v, ok := myMap["key1"]; ok {
fmt.Printf("Key1 exists and has a value of : %v\n", v)
}
// test if not exist ok != true
if _, ok := myMap["key4"]; !ok {
fmt.Println("Key4 does not exist")
}
Here’s the example on the playground: https://play.golang.org/p/nWRZIWJlCfh.
Deleting a key from a map
Delete is just as simple. We use the delete keyword and pass in both the map and the key to be deleted.
myMap := map[int]int{
1: 1,
2: 2,
3: 3,
}
fmt.Printf("%+v\n", myMap)
delete(myMap, 1)
delete(myMap, 2)
fmt.Printf("%+v\n", myMap)
The example is here for your reference: https://play.golang.org/p/aLjfW9nujnG.
Summary
In this tutorial, we looked at Go pointer semantics, and then three of Go’s composite types: arrays, slices and maps.
We looked at arrays, and how array length and capacity being part of their type wasn’t that useful to us at run-time.
We discussed how slices point to backing arrays which Go will mostly manage for us without us having to do too much - but that if we know the size we will need at run-time, it’s sensible to provide the capacity we need using the make keyword.
We covered how slices and maps are sometimes referred to as reference types and that we do not need to explicitly pass around their memory address in order to change them.
I want to put up this final example which shows what happens if we try to pass an array to a function and change an element.
An array is not a reference type. In order to change the contents of the original in an alter type function, we must pass the reference of the array to that function.
If we simply pass the value, the function receives a copy of the array, and our changes have no impact on the original.
Here is the example, and here is a link to the example in the Go Playground: https://play.golang.org/p/rtpmN8IQKIQ.
package main
import "fmt"
// pass in a copy of the value
func alterArray(tar [2]int, newVal int) {
tar[0] = newVal
}
// pass in the dereferenced address to get the value,
func alterArray2(tar *[2]int, newVal int) {
tar[0] = newVal
}
func main() {
var intArray [2]int
intArray[0] = 1
intArray[1] = 2
fmt.Printf("original : %+v\n", intArray)
alterArray(intArray, 20000)
// no change
fmt.Printf("no change : %+v\n", intArray)
alterArray2(&intArray, 20000)
// changed
fmt.Printf("changed : %+v\n", intArray)
}
Note when the array element value is changed, and, when it is not.
That’s all for this tutorial. I hope it was useful, simple and clear. Please share with other Gophers and post up your comments if you have any.
Until next week!
Ollie.