Goroutines: An Introduction to Go's Concurrency Primitives

Why so many prefer Go over C

What are Goroutines?

Go was designed at Google to handle large scale computational problems, and concurrency is baked into its very core. When we say "concurrency," we're referring to the ability of a program to handle multiple tasks at the same time. That's where Goroutines come in. A Goroutine is a lightweight thread of execution managed by the Go runtime.

// Defining a simple function

func sayHello() {

    fmt.Println("Hello, Go!")

}

// Running the function as a goroutine

go sayHello()

If you aren’t already taking weekly deep dives with me, subscribe below!

In this example, sayHello() would be executed as a Goroutine - denoted by go before the function call. However, when this code is run, there is no output because when the main function returns, all Goroutines are abruptly stopped. Let's fix that by asking it to wait for a second.

func sayHello() {

    fmt.Println("Hello, Go!")

}

go sayHello()

// Add some sleep in the main function

time.Sleep(time.Second)

This time, you should see "Hello, Go!" printed out. But relying on time.Sleep is not a good practice for synchronizing Goroutines. Instead, we use something called channels.

Channels are used to synchronize and exchange data between Goroutines. Let's look at a simple example where two Goroutines communicate through a channel:

func sayHello(ch chan string) {

    ch <- "Hello, Go!" // Send a value into the channel

}

ch := make(chan string)

go sayHello(ch)

message := <-ch // Receive a value from the channel

fmt.Println(message) // "Hello, Go!"

Here, we create a channel using make, pass it to our Goroutine, and then use it to communicate back to our main routine.

While we've explored the basics, Goroutines really shine when managing a larger number of tasks. Imagine we're running a web server where each incoming request is handled by a separate Goroutine, or we have a complex computational task that we can break down into smaller parts and run them concurrently.

Let's take a simple example of computing factorial of first N numbers concurrently using Goroutines:

func factorial(n int, ch chan int) {

    f := 1
    for i := 1; i <= n; i++ {

        f *= i

    }

    ch <- f

}

func main() {

    ch := make(chan int)

   
    for i := 1; i <= 20; i++ {

        go factorial(i, ch)

    }

    for i := 1; i <= 20; i++ {

        fmt.Println(<-ch) // This will print the factorials, but not necessarily in order

    }

}

In this program, we are spawning 20 Goroutines to calculate factorials of numbers from 1 to 20 concurrently. But, the output is not ordered as we might have expected. This is because Goroutines are not guaranteed to execute in the order they were started. This unpredictability is something to be aware of when designing your concurrent systems.

Goroutines and channels are fundamental to Go and enable a whole range of powerful concurrency patterns. They're like the power tools in your toolbox: when used with care and understanding, they can help you build impressive things.

[ Zach Coriarty ]

If you aren’t already taking weekly deep dives with me, subscribe below!