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?
A 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.
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.
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.
I hope you are now a bit comfortable with the concept of Goroutines. In the next chapter, we will deep dive into Goroutine internals.