This is the fifth article in our series in learning GO, the first four articles can be found here:
- It is time to “Go” and learn how to code with Go
- It is time to Go: our journey into grown-up code part 2
- It is time to Go: our journey into grown-up code part 3
- It is time to Go: our journey into grown-up code part 4
To date our journey has covered a lot of ground, and we have built up an understanding of several core features and concepts. We are truly building firm foundations.
- The core Variables types
- Variadic variables
- Redeclaration
- Constants
- inputs
- Structs
- Pointers
- Interfaces
- Function basics
- Conditional statements, if statements and switch statements.
- Loops, breaks, deferment and recover
At the end of our last article we had started dipping our toes into the Loops, we discussed the basic “for” loop and its constituent parts, we also investigated how Golang managed to emulated the concepts of a “while” and a “do-while” loop. How to handle special characters in a string statement, and what we can do to manage errors or to be precise a “panic”. To be even more precise, we learnt about the “nuclear option” of “defer function” and “recover”, which will “safely drop” out of a program; or to be more precise, smash the wall down with a sledgehammer and walk through the hole. And finally, we looked at the slightly less vicious concept of the “break” statement.
Today we will start to dive a bit deeper, so get out the scuba kit and let’s GO diving.
Today I will introduce a couple of more functions starting with “range”; we use this when we need to iterate over a slice of an array of items.
What is a range in GO
The range Keyword allows us to iterate over elements of an array, Slice, map or any other type of data structure. It is used in a loop or a switch to allow them to navigate through the data structure.
When iterating over array or slice elements, it returns the index of those elements in integer format. And the key of the key-value pair is returned when the range iterates over the elements of a Golang map; very useful when dealing with “string” based values in a map. Furthermore, the range can return a single value or multiple values. Let’s look at what the range returns in the Go programming language while iterating over various data structures in a loop.
package main import "fmt" func main() { odds := [10]int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19} for i, v := range odds { fmt.Printf("odds[%d] = %d \n", i, v) } }
Running this program will result in the following response:
There are several new things in here; firstly, we have introduced our first array as a part of the variable odds:
odds := [10]int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
wait what, I thought this was a slice. Well technically it could be, but because there is a value between the square brackets stating the number of entities that are contained between the curly brackets, it is an array. That said slices are a much more powerful construct than an array, as slices can represent a part of the same array, and multiple slices can share data items. This is not possible with an array. Therefore, you could say the array is the building block of a slice. Which makes sense as a slice of bread is a part of the whole loaf. The second new verb from the “fmt” package “%d” this is used to indicate the placement of an integer. What happens if we only have 9 values in the array?
odds := [10]int{1, 3, 5, 7, 9, 11, 13, 15, 17}
Changing the odds variable to read as above results in the following output.
That is correct it adds an entity with the value “0”. OK that makes sense because we have stated that the array will have 10 elements. Now what if we have 11 values in the array.
odds := [10]int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21}
Will the code be intelligent enough to strip the last entity?
The simple answer is no. however, I am impressed with the error message; to the point and not opaque in the slightest. Our array of 10 is out of bounds, as it has greater than 10 entities. This does raise the question of variable and constant validation. We will put that on to our list of things to consider later.
Let’s have a look at another loop.
package main import ( . "fmt" ) func main() { total := 0 for i := 0; i < 10; i++ { total += i } Println(total) }
Running this code produces the following:
This code produces the value 45 because it sums up all the numbers from 0 to 9, or to be precise 0+1+2+3+4+5+6+7+8+9 and prints the result to standard output. How does it do this? Firstly the variable “total” is initialized with the value “0” and then incremented by each value of “i” in the loop.
The construct in the loop is “total +=i” or to be precise add the current value of “i” to the previous value of “i” until the value of “i” is “10”, once that is reached the loop exits and the value is of the calculated “total” variable is printed using the “fmt.Println(total)” function.
The next piece of code we will look at will loop using a string, we also introduce a new package “reflect”. The Go reflect package gives us the features to inspect and manipulate an object at runtime (this means during the running of the program). Reflection is a very powerful tool for developers that broadens the scope of the GoLang programming language. It introduces three constructs “Types”, “Kinds”, and “Values” these constructs are used to gather information. This code will introduce the “reflect.ValueOf” construct, this construct has a number of methods of finding out information about the value of a variable.
package main import ( "fmt" "reflect" ) type User struct { FirstName string Lastname string Email string Phone int } func main() { u := User{ FirstName: "John", Lastname: "Doe", Email: "johndoe@gmail.com", Phone: 1234567, } values := reflect.ValueOf(u) typesOf := values.Type() for i := 0; i < values.NumField(); i++ { fmt.Printf("Field: %s\tValue: %v\n", typesOf.Field(i).Name, values.Field(i).Interface()) } }
Running this code will result in the following output:
OK so lets have a look at what is going on, as this is the first time we have looked at a loop with a string variable as an input.
We have already mentioned the importation of the “reflect” package, and what additional functionality it brings to our code, we will examine where we use it later.
The first thing we are creating as a “struct” we discussed “structs” in our second article. This time we are creating our struct “user” with four values, three strings (FirstName, LastName and Email) and one integer (Phone).
We then enter the “main” function and initialise a variable “u” with the struct with the following values.
u := User{ FirstName: "John", Lastname: "Doe", Email: "johndoe@gmail.com", Phone: 1234567, }
The following line is important as this creates a variable “value” by running the function “reflect.ValueOf” with the value of the variable “u”.
values := reflect.ValueOf(u)
We then create a variable “typesOf” with the contents of the variable “u” as gathered by using the content of the aforementioned variable “values”.
typesOf := values.Type()
This converts the four strings contained in the struct to four integers, then contains the “strings”, this may not make sense, so it is probably better to display this out.
[1] FirstName: "John", [2] Lastname: "Doe", [3] Email: "johndoe@gmail.com", [4] Phone: 1234567,
We now start to look at the main part of the function the “for” loop.
for i := 0; i < values.NumField(); i++ { fmt.Printf("Field: %s\tValue: %v\n", typesOf.Field(i).Name, values.Field(i).Interface())
Looking at the for loop construct you will notice that the value of the condition statement is different. This loop is controlled by a function of the “reflect” function. “values.NumField()”. This function can only be used with “structs” and any attempt to use it with a different construct will result in a panic.
i < values.NumField()
As there are four values in the variable the loop will run four times and exit.
fmt.Printf("Field: %s\tValue: %v\n", typesOf.Field(i).Name, values.Field(i).Interface())
A lot of this line should be starting to be recognisable by now. We are printing to standard output “fmt.Printf” the details between the “()”, OK, what is happening between the brackets? This looks complicated at first pass. But it is actually quite simple. We already understand that “Printf” formats its outputs according to what format specifiers are defined. In this case, it prints a string with two placeholders: “%s” and “%v”. “%s” is replaced by the name of the field and “%v” is replaced by the value of the field. That sounds simple, but what are the values of “%s” and “%v”. in the table below I have broken down the necessary values as we can see the function “typesOf.Field(i).Name” returns the value of the name column at the necessary index “i” this equates to the “%s” format specifier (remember this is a string format specifier). The second value “%v” (remember that “%v” is the default specifier) of the struct “typesOf”, while “values.Field(i).Interface()” returns the value of that field.
Integer | Name (%s) | Interface (%v) |
[1] | FirstName: | “John” |
[2] | Lastname: | “Doe” |
[3] | Email: | “johndoe@gmail.com” |
[4] | Phone: | 1234567 |
I have to say that the “reflect” function seems very useful. It is time to introduce another useful function “sync” this core package provides synchronisation primitives to go, things like mutex, Waitgroup, Pool and condition variables; we will primarily be looking at the “Sync” function “Waitgroup”. However, as we have already mentioned them, we will briefly touch on them. In plain English, a mutex is a lock that allows only one goroutine to access a shared resource at a time. It is used to prevent race conditions in concurrent programs. The “sync.Mutex” package in Go provides a mutex primitive that can be used to synchronize access to shared resources in a concurrent program. This raises the next question what exactly is a race condition. Clue it is not the 100 meters.
A race condition is a situation that occurs when two or more goroutines access a shared resource concurrently and try to modify it at the same time. This can lead to unpredictable behaviour in the program. In Go, mutexes are used to prevent race conditions by allowing only one goroutine to access a shared resource at a time. We will be looking at these later. Sync also has a construct called a pool.
In Go, a Pool is a set of temporary objects that can be saved and retrieved individually. The sync.Pool package provides a Pool that can be used to amortize allocation overhead across many clients. An example of good use of a Pool is in the “fmt” package, which maintains a dynamically-sized store of temporary output buffers. The last we function will give a brief overview of is the conditional variable. In plain English, it is a method for Go to signal between goroutines.
But what about the WaitGroup? In Go, a “WaitGroup” is a struct that is part of sync package. It is generally used to wait for a collection of goroutines to finish execution. WaitGroup comes with 3 methods namely: Add (int): Indicates the number of goroutines to wait for. The Add () function takes an integer as a parameter and this integer acts as a counter. Wait (): Blocks a block of code, until the internal counter reduces to zero. Let’s have a look at “WaitGroup” in action.
Package main Import ( “fmt” “sync” ) func main(){ var wg = &sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func() { fmt.Println(i) wg.Done() }() } wg.Wait() }
This package creates 10 goroutines and waits for them to finish. The “sync.WaitGroup” is used to wait for all goroutines to finish before exiting. We create 10 goroutines that print out the value of “i” which is incremented in each iteration of the loop.
What is interesting is that the output of this code is not what you might expect. Instead of printing the numbers 0 through 9, it will print the number 10 ten times. This is because each goroutine shares the same variable “i”, which is incremented in the for loop. By the time each goroutine runs, “i” has already been incremented to 10. We can prove this by running the code, and Yes, we receive the result show below.
So what exactly is happening here. This is quite a complicated piece of code, with a lot of moving parts. Firstly, we first create a variable “wg” by creating a new instance of a “WaitGroup” struct, “&sync.WaitGroup{}” creates our new instance struct. It is the WaitGroup struct that is used to synchronize our goroutines in Go, or basically pausing the final output until all iterations have been completed. It provides the method to wait for a collection of goroutines to finish executing before continuing. The “&” symbol is used to create a pointer to the new instance of the struct.
However, the clever part here is the addition of the “go func()” this is a way to start a new goroutine in Go. Goroutines are lightweight threads that allow you to run multiple functions concurrently. The go keyword is followed by a function call, which is then executed concurrently in a new goroutine. In our example code, the function call is “fmt.Println(i)” and “wg.Done()”. Now as intimated above you would think that the “i” in the “fmt.Println(i)” statement would iterate by one each loop, but no because this section of the loop is effectively paused until all iterations have run, and the “i” actually equals 10 by the time the process continues.
One thing to note is that it is very important that “wg.Add()” is before the “go func()” as this prevents a race condition. Also “wg.Add()” must come before “wg.Done()”. This is because “wg.Done()” is not an exit statement for the loop but a statement to return the “wg.Add()”. It is the “wg.Add()” that must be full or contain the 10 iterations before the loop will run. The final part is the “wg.wait()” method.
But what if we wanted to actually have the output be 0-9, rather than the 10 that is currently received?
To fix this issue, you can pass “i” as an argument to each goroutine:
func main() { var wg = &sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() }
This will print the numbers 0 through 9 as expected.
So why is this not in order? Well remember this output is to be expected, because we have used a “waitgroup” and created 10 goroutines, however due to the vagaries of processing, we cannot guarantee the individual goroutines output positions.
Summary
This has been quite a hard article, we have once again covered plenty of new ground. We have started to dive a little deeper looking at new built-in packages to add functionality to our loops. As with the previous articles, I have once again been left with many more questions than answers. So the journey continues.
If you have any questions or ideas of what to investigate in this journey, please add a comment or email.