DEV Community

Cover image for Goroutines: Understanding Concurrency in Go
Boluwatife Olaifa
Boluwatife Olaifa

Posted on • Originally published at busta.hashnode.dev

Goroutines: Understanding Concurrency in Go

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.

Goroutines in threads

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Here is the output on the second run:

main
0123456789Hello world
Goodbye world
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)