Skip to main content

Goroutines

Goroutines in Go

The Beauty in Go

Goroutine is one of the most basic units of organization in a Go program, so we must understand what they are how they work, and why we need them.

What is a Goroutine?

goroutine is a lightweight execution thread in the Go programming language and a function that executes concurrently with the rest of the program.

Goroutines are incredibly cheap when compared to traditional threads as the overhead of creating a goroutine is very low. Therefore, they are widely used in Go for concurrent programming.

To invoke a function as a goroutine, use the go keyword.

Syntax

go foo()
We write go before the function foo to invoke it as a goroutine. The function foo() will run concurrently or asynchronously with the calling function.

Let's look at the below code.

package main

import (
	"fmt"
	"time"
)

// Prints numbers from 1-3 along with the passed string
func foo(s string) {
	for i := 1; i <= 3; i++ {
		fmt.Println(s, ": ", i)
	}
}

func main() {

	// Starting two goroutines
	go foo("1st goroutine")
	go foo("2nd goroutine")

	// Wait for goroutines to finish before main goroutine ends
	time.Sleep(time.Second)
	fmt.Println("Main goroutine finished")
}

Output

2nd goroutine :  1
2nd goroutine :  2
2nd goroutine :  3
1st goroutine :  1
1st goroutine :  2
1st goroutine :  3
Main goroutine finished

If you run it multiple times, you will notice that either 1st Goroutine will be finished first or 2nd Goroutine completed first. But you may think if both Gortoutines are running in parallel then why every time one of them finishes first?

The reason is simple: this is because we have a very small number of values, so whichever Goroutine is spawned first it finishes off quickly. To see them running asynchronously, we can add some delay while printing values.

Updated Code:

package main

import (
	"fmt"
	"time"
)

// Prints numbers from 1-3 along with the passed string
func foo(s string) {
	for i := 1; i <= 3; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s, ": ", i)
	}
}

func main() {

	// Starting two goroutines
	go foo("1st goroutine")
	go foo("2nd goroutine")

	// Wait for goroutines to finish before main goroutine ends
	time.Sleep(time.Second)
	fmt.Println("Main goroutine finished")
}

Output

2nd goroutine :  1
1st goroutine :  1
2nd goroutine :  2
1st goroutine :  2
2nd goroutine :  3
1st goroutine :  3
Main goroutine finished

If you run it multiple times, you might see different outputs.

💡
If you are a beginner and this is overwhelming you. Then take a deep breath. Things will fall into place. The only thing you need to do is try running every exercise on your machine. Take a pause and understand, re-read twice, thrice, and maybe multiple times till you don't get the concepts. Concurrency is easy once you understand why and how.

Let's move forward.

Have you noticed we have put one statement i.e. time.Sleep(time.Second) at the last of the program? Just remove that line and then try running it. You will be amazed to see the output.

Output

Main goroutine finished

I hope you've also got the same output. Let's try to understand, why it happened.

In this program, two Goroutines (1st Goroutine and 2nd Goroutine) are concurrently started using the go keyword inside the main function. The foo function, which is executed by each Goroutine, prints numbers from 1 to 3 along with the provided string.

However, the main Goroutine doesn't wait for the two spawned Goroutines to finish their execution. As a result, it continues its execution independently and prints Main goroutine finished without waiting for the other Goroutines to complete.

💡
Note: The time.Sleep() function is just a workaround. Don’t use it to make your main goroutine complete its functions. Instead, use WaitGroup.

The purpose of Goroutine

Because we now know how to use Goroutines in Go. Let's try to understand the purpose of Gouroutine.

Consider a scenario where you need to make a GET request and process the response. However, there are additional steps in the same code block that are unrelated to the HTTP requests. Executing the code sequentially may lead to inefficiencies, especially when waiting for the GET request's response.

To address this, Golang provides an elegant solution using goroutines. By placing the HTTP request code in a separate goroutine, it can execute independently, allowing the rest of the code to proceed without waiting for the response. This approach enhances efficiency by enabling parallel execution of tasks.

In practical terms, the GET the request is initiated in a separate goroutine, allowing it to run concurrently with the remaining code that is unrelated to HTTP requests. This not only ensures better performance but also simplifies the code structure.

By leveraging goroutines, Golang enables a straightforward and understandable way to handle parallelism, enhancing the overall efficiency of code execution. This approach aligns with Golang's concurrency model, emphasizing simplicity and clarity in concurrent programming.

To understand it, let's take a very simple example where we want to print the addition and multiplication of two numbers within the same block of code. To achieve this, we have created two different methods Add() and Multiply().

package main

import (
	"fmt"
	"time"
)

func Add(a int, b int) {
	fmt.Printf("Sum = %d\n", a+b)
	time.Sleep(time.Second * 2)
}

func Multiply(a int, b int) {
	fmt.Printf("Multiplication = %d\n", a*b)
	time.Sleep(time.Second * 2)
}

func main() {
	start := time.Now()
	Add(5, 4)
	Multiply(5, 4)
	fmt.Printf("Time taken to perform both operations: %s", time.Since(start))
}

Output

Sum = 9
Multiplication = 20
Time taken to perform both operations: 4.002483292s

In the code above, the running time is around four seconds. However, we know that the multiplication in the code above has nothing to do with the addition. Both the goroutines are capable of executing independently. But since the code executes sequentially, we have to wait twice as long since time.Sleep adds to the total execution time.

What Goroutine can do here?

package main

import (
	"fmt"
	"sync"
	"time"
)

func Add(a int, b int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Sum = %d\n", a+b)
	time.Sleep(time.Second * 2)
}

func Multiply(a int, b int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Multiplication = %d\n", a*b)
	time.Sleep(time.Second * 2)
}

func main() {
	start := time.Now()
	var wg sync.WaitGroup
	wg.Add(2)

	go Add(5, 4, &wg)
	go Multiply(5, 4, &wg)

	wg.Wait()
	fmt.Printf("Time taken to perform both operations: %s", time.Since(start))
}

Output

Multiplication = 20
Sum = 9
Time taken to perform both operations: 2.001522583s

In the code above, the running time is only about two seconds because two functions are executing parallel to each other. Now, even though time.Sleep is running twice, each is executing in its separate goroutine.

💡
Note: Alternatively, we could have used time.Sleep(time.Second * 3) instead of WaitGroups. However, in that case, it wouldn't have been as intuitive. Additionally, with this approach, the main goroutine would have to wait for at least 3 seconds before proceeding, regardless of whether the spawned goroutines have completed their execution.

I hope you are now a bit comfortable with the concept of Goroutines. In the next chapter, we will deep dive into Goroutine internals.