knowledge.txt

Sharing my knowledge on security topics.

View on GitHub

Managing Concurrent Tasks with Goroutines and WaitGroups

According to the official Go documentation, “A goroutine is a lightweight thread managed by the Go runtime.”

In simpler terms, Goroutines are functions that detach themselves from the main function, allowing them to run concurrently and independently of it. To execute any function as a Goroutine, you use the keyword go before calling the function, like this: go foo().

For example, run the code below:

package main

import (
	"fmt"
)

func  goroutines(){
	fmt.Println("I'm a simple Goroutine")
}

func  main() {
	fmt.Println("I'm the main function")
	go  goroutines()
}

Try on Playground

You may notice that the line fmt.Println("I'm a simple Goroutine") inside the Goroutine function never gets executed. Why? As I mentioned earlier, Goroutines detach themselves from the main function. Therefore, when the main function finishes its execution, all Goroutines also stop executing, leading to the program’s premature exit. However, if you add a small delay, as shown below, you will observe that our Goroutine gets executed.

package main

import (
	"fmt"
	"time"
)

func  goroutines(){
	fmt.Println("I'm a simple Goroutine")
}

func  main() {
	fmt.Println("I'm the main function")
	go  goroutines()
	// Adding a brief delay to demonstrate Goroutine execution
	time.Sleep(2  * time.Second)
}

Try on Playground

While in the above code, we added a time delay to prevent the program from exiting immediately, it’s not always feasible to predict the exact duration of a program’s execution. To solve this problem, we can use a WaitGroup.

What are the Waitgroups in Golang?

As the name suggests, WaitGroups in Golang allow us to wait for all our Goroutines to finish their execution before the program completes.

In Golang, the WaitGroup is a part of the standard package and can be imported from the sync package.

Here are the methods provided by WaitGroups:

Solving our problem with Waitgroups:

package main

import (
	"fmt"
	"sync"
)

func  goroutines(wg *sync.WaitGroup){
	defer wg.Done()
	fmt.Println("I'm a simple Goroutine")
}

func  main() {
	var  wg sync.WaitGroup
	
	fmt.Println("I'm the main function")

	wg.Add(1)
	go  goroutines(&wg)

	wg.Wait()
}

Try on Playground

Let’s break down this code. In the main function, we declare a WaitGroup named wg using var wg sync.WaitGroup. We then add one counter to the WaitGroup using wg.Add(1) and pass a pointer to wg in our goroutines function.

Inside the func goroutines(wg *sync.WaitGroup), we use defer wg.Done() to call wg.Done() after the function completes its execution.

Finally, we use wg.Wait() to make the main function wait for all Goroutines to finish their execution.

Compared to the previous solution using time.Sleep, which involved an arbitrary time delay to ensure the main function doesn’t finish execution before the Goroutines are done, using WaitGroup allows the main function to wait for all Goroutines to complete their execution.

What Makes Goroutines a Good Fit for Security Automation?

Goroutines provide significant benefits for building performant and scalable security tools. As lightweight threads of execution, goroutines enable concurrent operations and parallelism without the overhead of operating system threads. This allows security tools written in Go to handle many simultaneous tasks, connections, and requests efficiently.

If you were to develop a tool that retrieves subdomains from two sources, where one source takes approximately 8 seconds and the other takes 4 seconds to fetch subdomains domains, running the tool without utilizing goroutines and waitgroups would result in a total execution time of 12 seconds. However, with goroutines and waitgroups you can complete the task in 8 seconds..

package main

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

func extractDomainsFromA(target string, wg *sync.WaitGroup) {
	time.Sleep(4  * time.Second)
	defer wg.Done()

	fmt.Println("Done extracting domians from source A")
}


func extractDomainsFromB(target string, wg *sync.WaitGroup) {
	time.Sleep(8  * time.Second)
	defer wg.Done()
	
	fmt.Println("Done extracting domians from source B")
}

func main() {
    targetName := flag.String("target", "", "Specify the target domain (e.g., india.gov.in)")
	flag.Parse()

	if *targetName == "" {
			fmt.Println("Please provide a target domain using the -target flag.")
			return
	}

	target := *targetName
	var wg sync.WaitGroup

	wg.Add(2)
	go extractDomainsFromA(target, &wg)
    go extractDomainsFromB(target, &wg)

	wg.Wait()
}

visualize-goroutine

In this approach, the efficiency of the entire code is determined by its slowest component, often referred to as the ‘weakest link,’ which, in turn, impacts the overall execution time. While we have explored the use of Goroutines and Waitgroups to enable asynchronous execution, you might be curious about how to share data between Goroutines.

As I learn Golang, I’m eager to share my ongoing journey with you. Probably the next blog will be about channels in Golang but it might take a little time.