Menu Close

Go – How to use Nested Goroutines in Go

Here, we are going to learn How to use Nested Goroutines in Go with example and their benefits in details.

Nested goroutines are a powerful feature of Go programming language that allow developers to create complex concurrent applications.

What are Nested Goroutines?

A goroutine is a lightweight thread of execution in Go that allows developers to run functions concurrently. Goroutines are managed by the Go runtime and are created using the go keyword followed by the function call. One of the advantages of goroutines is that they are cheap to create and can be used to perform tasks asynchronously.

Nested goroutines, as the name suggests, are goroutines that are created within another goroutine. In other words, a goroutine can spawn multiple child goroutines, each of which can also spawn more goroutines. This creates a hierarchical structure of goroutines, where the parent goroutine is responsible for managing the child goroutines.

Benefits of Nested Goroutines

The main benefit of nested goroutines is that they allow developers to create complex concurrent applications that can handle multiple tasks simultaneously. By using nested goroutines, developers can create a hierarchy of tasks, where each level can be responsible for performing a specific set of operations. This results in more efficient use of system resources, as the application can perform multiple tasks concurrently.

Another advantage of nested goroutines is that they allow for better error handling. Since each goroutine is responsible for a specific set of tasks, if an error occurs in one goroutine, it can be handled locally without affecting other goroutines. This makes it easier to debug and isolate errors, resulting in more robust and stable applications.

Example of Nested Goroutines

Example 1:

Let’s consider an example to illustrate how nested goroutines can be used in a real-world scenario. Suppose we want to build an application that performs the following tasks:

  1. Reads data from a file.
  2. Parses the data.
  3. Performs some computations on the parsed data.
  4. Stores the computed results in a database.

To perform these tasks concurrently, we can use nested goroutines as follows:

func main() {
    // create a channel to communicate between goroutines
    ch := make(chan []byte)

    // start a goroutine to read data from the file
    go func() {
        data, err := ioutil.ReadFile("data.txt")
        if err != nil {
            log.Fatal(err)
        }
        ch <- data
    }()

    // start a goroutine to parse the data
    go func() {
        data := <-ch
        parsedData, err := parseData(data)
        if err != nil {
            log.Fatal(err)
        }
        ch <- parsedData
    }()

    // start a goroutine to perform computations
    go func() {
        parsedData := <-ch
        computedResults, err := performComputations(parsedData)
        if err != nil {
            log.Fatal(err)
        }
        ch <- computedResults
    }()

    // start a goroutine to store the results in the database
    go func() {
        computedResults := <-ch
        err := storeResults(computedResults)
        if err != nil {
            log.Fatal(err)
        }
    }()

    // wait for all goroutines to finish
    time.Sleep(5 * time.Second)
}

func parseData(data []byte) ([]byte, error) {
    // parse data here
}

func performComputations(parsedData []byte) ([]byte, error) {
    // perform computations here
}

func storeResults(computedResults []byte) error {
    // store results in database here
}

In this example, we create a channel ch to communicate between goroutines. We then spawn four goroutines to perform.

Example 2: Parallel Matrix Multiplication

To illustrate the concept of nested goroutines, let’s look at a parallel matrix multiplication example. We will use two matrices, A and B, and calculate their product, C, using nested goroutines.

package main

import (
	"fmt"
	"sync"
)

func main() {
	A := [][]int{
		{1, 2},
		{3, 4},
	}
	B := [][]int{
		{5, 6},
		{7, 8},
	}

	rowsA := len(A)
	colsA := len(A[0])
	rowsB := len(B)
	colsB := len(B[0])

	if colsA != rowsB {
		panic("The number of columns in A must be equal to the number of rows in B")
	}

	var C [][]int = make([][]int, rowsA)
	for i := range C {
		C[i] = make([]int, colsB)
	}

	var wg sync.WaitGroup
	wg.Add(rowsA * colsB)

	for i := 0; i < rowsA; i++ {
		for j := 0; j < colsB; j++ {
			go func(i, j int) {
				defer wg.Done()
				for k := 0; k < colsA; k++ {
					C[i][j] += A[i][k] * B[k][j]
				}
			}(i, j)
		}
	}

	wg.Wait()
	
	fmt.Println("Matrix A:")
	printMatrix(A)
	
	fmt.Println("Matrix B:")
	printMatrix(B)
	
	fmt.Println("Matrix C (A x B):")
	printMatrix(C)
}

func printMatrix(matrix [][]int) {
    
	for _, row := range matrix {
		for _, val := range row {
			fmt.Printf("%d ", val)
		}
		fmt.Println()
	}
}

Output:

Matrix A:
1 2 
3 4 

Matrix B:
5 6 
7 8 

Matrix C (A x B):
19 22 
43 50 

Explanation:

  • We first define two matrices, A and B, and initialize their values.
  • We then determine the dimensions of the matrices.
  • Next, we check if the number of columns in A is equal to the number of rows in B, as required for matrix multiplication.
  • We create a result matrix C with the same number of rows as A and columns as B.
  • We utilize a sync.WaitGroup (wg) to keep track of the number of goroutines we need to wait for before continuing.
  • We start two nested loops, iterating through each row of A and each column of B.
  • For each cell in the result matrix C, we create a new goroutine that calculates the value of that cell by iterating through the respective row of A and column of B and summing the product of the corresponding elements.
  • Once all the goroutines are completed, we print the matrices A, B, and C.

Example 3:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Main goroutine started")
    go parent()
    time.Sleep(5 * time.Second)
    fmt.Println("Main goroutine ended")
}

func parent() {
    fmt.Println("Parent goroutine started")
    go child1()
    go child2()
    time.Sleep(2 * time.Second)
    fmt.Println("Parent goroutine ended")
}

func child1() {
    fmt.Println("Child1 goroutine started")
    time.Sleep(1 * time.Second)
    fmt.Println("Child1 goroutine ended")
}

func child2() {
    fmt.Println("Child2 goroutine started")
    time.Sleep(1 * time.Second)
    fmt.Println("Child2 goroutine ended")
}

In this example, we have a main goroutine that spawns a parent goroutine. The parent goroutine, in turn, spawns two child goroutines (child1 and child2) and sleeps for 2 seconds before printing a message to indicate that it has ended.

The child1 and child2 goroutines simply sleep for 1 second before printing a message to indicate that they have ended.

When we run this program, we should see the following output:

Main goroutine started
Parent goroutine started
Child1 goroutine started
Child2 goroutine started
Child1 goroutine ended
Child2 goroutine ended
Parent goroutine ended
Main goroutine ended

As we can see, the parent goroutine starts the child1 and child2 goroutines, which run concurrently with each other and with the parent. The parent goroutine then waits for 2 seconds before printing a message to indicate that it has ended. Finally, the main goroutine waits for 5 seconds before printing a message to indicate that it has ended.

Conclusion

Nested goroutines are a powerful feature of Go’s concurrency model that can be used to create parallelism and improve the performance of concurrent programs. By spawning additional goroutines as needed, we can create a hierarchy of parallelism that allows us to process tasks more quickly and efficiently.

To learn more about golang, Please refer given below link:

References:

https://golang.org/pkg/

Posted in golang

Leave a Reply

Your email address will not be published. Required fields are marked *