Concurrency is one of the strongest tools in Go but how to use them? Well just put the keyword go before a function. It can’t be that easy, does it? Well, not really. Let’s see some concurrency patterns in Go.

Introduction

Let’s write a simple code

package main 
import "fmt"

func main() {
    count("Hello")
}

func count(str string) {
    for i := 0; i < 5; i++ {
        fmt.Println(str)
    }
}

On running this, we get a simple output of

❯ go run "d:\Code\GolangProjects\go-cocurency-test\main.go"
Hello
Hello
Hello
Hello
Hello

Let’s follow the mentioned statement and spawn a goroutine using go the keyword

func main() {
    go count("Hello")
}

In the output, we get nothing, its empty???

❯ go run "d:\Code\GolangProjects\go-cocurency-test\main.go"

Well, its because it did spawn a new thread but no one told the main thread to wait for it. Let’s just add a sleep after the main function to let it wait for the goroutine to complete.

func main() {
    go count("Hello")
    time.Sleep(time.Second)
}

Now we can see all Hello getting printed properly

❯ go run "d:\Code\GolangProjects\go-cocurency-test\main.go"
Hello
Hello
Hello
Hello
Hello

Wait Group

Now using sleep is ok but one can’t expect to know when the function will get over. What we can do is use the Sync package and make a wait group.

func main() {
	
	// to know how much time our function take
	now := time.Now()
    defer func() {
        fmt.Println(time.Since(now))
    }()

    var wg sync.WaitGroup
    
    wg.Add(1)

    names := []string{"Achintya", "Master Chief", "Solid Snake"}

	go func() {
        for _, name := range names {
            count(name)
        }
        wg.Done()
    }()
    wg.Wait()
}

func count(str string) {
    fmt.Println("Hello from ", str)
    time.Sleep(time.Second)
}

Lemme explain the code a little bit

  • Defer func is a function that will execute at the end of the main function.

  • We will initialise a variable wg which will help us track whether the function is complete and wait for it.

  • wg.Add(1) : It actually adds a delta of 1 to the waiting group and says to wait.

  • After the for loop, I used them wg.Done() to tell the runtime that the task of this anonymous function is complete. It will decrement the delta by 1.

  • In the end, we have wg.Wait(). It will wait and keep the main function running until the value of the delta becomes 0.

  • Also, I have added a time.Sleep() count function so as to make it feel like a real-world scenario.

Now the output would be here as

❯ go run "d:\Code\GolangProjects\go-cocurency-test\main.go"
Hello from  Achintya
Hello from  Master Chief
Hello from  Solid Snake
3.0329866s

And our code is working fine but we are just spawning 1 thread. How about spawning a new thread for every iteration of for loop? We gotta take advantage of goroutines. Let’s change the code a little.

var wg sync.WaitGroup

func main() {
    now := time.Now()

    defer func() {
        fmt.Println(time.Since(now))
    }()

    names := []string{"Achintya", "Master Chief", "Solid Snake"}

    for _, name := range names {
        wg.Add(1)
        go count(name)
    }
    
    wg.Wait()
}

func count(str string) {
    fmt.Println("Hello from ", str)
    time.Sleep(time.Second)
    
    defer func ()  {
        wg.Done()
    }()
}

Here we made a new goroutine in each iteration of for loop. I have added wg.Add(1) and then completed the wg.Done() in the function call using defer. Now even if all the function is supposed to take 3 seconds, we through goroutines have completed in almost 1 second.

❯ go run "d:\Code\GolangProjects\go-cocurency-test\main.go"
Hello from  Solid Snake
Hello from  Achintya
Hello from  Master Chief
1.008593s

This will be part 1. In later parts, we will study about channels in Go.