Goroutines: Understanding Concurrency in Go
Boluwatife Olaifa
Posted on February 7, 2023
One of the reasons Go is popular is its fascinating support for concurrency. Concurrency is the ability of a program to perform multiple operations at once. This is essential because we are now in a world where speed is a priority.
Go is packed with features like Goroutines, Channels, and Select. These features enable developers to build faster and more highly scalable applications.
What is a Goroutine?
A Goroutine is an entity that can be executed concurrently and independent of other functions.
Whenever you start your Go program, a process is created on your computer. This process is just an environment that contains instructions and all the resources needed to run your program. A process creates a thread that is smaller and lighter. Goroutines are extremely lightweight and they live in the thread of an operating system.
Creating a Goroutine does not require a lot of memory. Each Goroutine consumes only 2kB of stack space. Thus, running hundreds or thousands of them is not a problem. Since Goroutines are lighter than threads and threads are lighter than processes, it gives us the ultimate speed.
Creating a Goroutine
Creating a Goroutine is as easy as adding a special keyword called go
before your function call. Almost any function can be called with the keyword.
Below is an example of what a Goroutine looks like:
package main
import (
"fmt"
"time"
)
func helloWorld() {
for i := 0; i < 10; i++ {
fmt.Print(i)
}
fmt.Println("Hello world")
}
func goodByeWorld() {
fmt.Println("Goodbye world")
}
func main() {
go helloWorld()
go goodByeWorld()
fmt.Println("main")
time.Sleep(1 * time.Second)
}
The program above contains two functions: helloWorld()
and goodByeWorld()
. A for
loop was added in helloWorld()
just to simulate the effect of Goroutines. Adding a time.Sleep
of 1 second ensures that all our function runs before the program exits.
What the go
keyword does in the program above is just tell the program to move on to the next task while the helloWolrd()
and goodByeWolrd()
function runs in the background.
Here is what the output looks like on the first run:
main
Goodbye world
0123456789Hello world
Here is the output on the second run:
main
0123456789Hello world
Goodbye world
As seen in the outputs above, the results are not in the order they were created in the code and they are not deterministic. Why? Because of the introduction of the go
keyword. This means that the order in which Goroutines will be executed cannot be controlled without taking extra care. Extra care means writing extra code.
Waiting for a Goroutine to finish
In the section above, there is a part of the code that looks like this:
func main() {
go helloWorld()
go goodByeWorld()
fmt.Println("main")
time.Sleep(1 * time.Second)
}
The time.Sleep
function delays the program to ensure all Goroutine runs before the program exits. This is not ideal at the production level because running millions of concurrent requests can cause problems and the time it will take all goroutines to complete can’t exactly be predicted.
Solving this requires the use of sync.WaitGroup
like in the code below:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func helloWorld() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Print(i)
}
fmt.Println("Hello world")
}
func goodByeWorld() {
defer wg.Done()
fmt.Println("Goodbye world")
}
func main() {
wg.Add(2)
go helloWorld()
go goodByeWorld()
fmt.Println("main")
wg.Wait()
}
In the code above, a variable wg
of type sync.WaitGroup
was declared.
wg.Add(2)
increases WaitGroup count by 2, which tells the program to wait for 2 goroutines: helloWorld()
and goodByeWorld()
.
wg.Done()
reduces the count by 1, meaning that one of the functions is done running. A defer
keyword is added to ensure the helloWorld()
and goodByeWorld()
function is only marked as done after completion.
wg.Wait()
is a blocking command to make the program wait until all goroutines are completed i.e the counter is 0.
Here is what the output looks like:
main
Goodbye world
0123456789Hello world
The output is the same as before, but this time we are not blocking the program with time.Sleep
for 1 second. The sync.WaitGroup
does all the magic.
Conclusion
This article covered the basics of concurrency in Go. There are more concepts such as Channels, Mutexes and Pipelines. Hopefully, I will be shedding more light on these concepts in my future writings.
Posted on February 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.