Concurrency in Go: A Practical Guide with Hands-On Examples
Tags: webdev, go, beginners, programming
Published: 2023-08-26 11:20:44

## Introduction
Navigating the realm of concurrency in programming can be like orchestrating a symphony of activities that occur simultaneously. Enter the world of Go, a programming language that gracefully handles concurrency through goroutines and channels. In this blog, we'll journey through a series of hands-on examples, each illustrating an essential lesson in harnessing the power of concurrency in Go.

### Lesson 1: Starting with the Basics
```go
func infiniteCount(thing string) {
	for i := 1; true; i++ {
		fmt.Println(i, thing)
		time.Sleep(time.Second * 1)
	}
}

// The program counts "dog" forever and never gets to "cat".
func main() {
	infiniteCount("dog")
	infiniteCount("cat")
}
```
#### Output
```
~ go run .
1 dog
2 dog
3 dog
4 dog
```
In our first lesson, we get acquainted with the concept of concurrency. The function `infiniteCount` continuously prints out numbers along with a given text, providing us with a simple example of a recurring task. However, the call to `infiniteCount("cat")` never occurs after starting with `infiniteCount("dog")`, highlighting how concurrency can affect program execution.

### Lesson 2: Introducing Goroutines
```go
func infiniteCount(thing string) {
	for i := 1; true; i++ {
		fmt.Println(i, thing)
		time.Sleep(time.Second * 1)
	}
}

// Using goroutines: "dog" is counted in the background.
func main() {
	go infiniteCount("dog")
	infiniteCount("cat")
}
```
#### Output
```
~ go run .
1 cat
1 dog
2 dog
2 cat
3 cat
3 dog
```
In lesson two, we dive into _**goroutines**_, Go's concurrency units. By using the `go` keyword before a function call, we're able to launch concurrent execution. We start counting "dog" concurrently and immediately proceed to counting "cat". This showcases the non-blocking nature of goroutines.

### Lesson 3: The Challenge of Asynchrony
```go
func infiniteCount(thing string) {
    for i := 1; true; i++ {
        fmt.Println(i, thing)
        time.Sleep(time.Second * 1)
    }
}

// Running both count functions as goroutines.
func main() {
	go infiniteCount("dog")
	go infiniteCount("cat")
}
```
#### Output
```
~ go run .
~
```
Expanding on the previous lesson, we explore the scenario where both `infiniteCount("dog")` and `infiniteCount("cat")` are run as goroutines. Surprisingly, the expected output doesn't appear. Why? Because the program's main function exits before the goroutines finish execution, leading to an incomplete run.

### Lesson 4: Synchronization with WaitGroups
```go
func count(thing string) {
	for i := 1; i <= 5; i++ {
		fmt.Println(i, thing)
		time.Sleep(time.Millisecond * 500)
	}
}

// Employing sync.WaitGroup to wait for goroutines.
func main() {
	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		count("dog")
		wg.Done()
	}()

	wg.Wait()
}
```
#### Output
```
~ go run .
1 dog
2 dog
3 dog
4 dog
5 dog
~
```
To address the synchronization challenge posed in the previous lesson, we introduce the `sync.WaitGroup`. This construct helps us ensure that all **_goroutines_** finish before the program terminates. With `sync.WaitGroup`, we can synchronize goroutines' execution, waiting for their completion using `wg.Wait()`.

### Lesson 5: Communicating via Channels
```go
func countWithChannel(thing string, c chan string) {
	for i := 1; i <= 5; i++ {
		c <- thing
		time.Sleep(time.Millisecond * 500)
	}
	close(c)
}

// Leveraging channels for communication.
func main() {
	c := make(chan string)
	go countWithChannel("dog", c)

	for msg := range c {
		fmt.Println(msg)
	}
}
```
#### Output
```
~ go run .
dog
dog
dog
dog
dog
~
```
Moving beyond isolated **_goroutines_**, we delve into **_channels_**, the communication mechanism between concurrent processes in **Go**. We modify the `countWithChannel` function to send messages through channels. In lesson5, we create a channel, pass it to the function, and receive and print messages using a loop.

### Lesson 6: Escaping Channel Deadlocks
```go
func countWithChannel(thing string, c chan string) {
	for i := 1; i <= 5; i++ {
		c <- thing
		time.Sleep(time.Millisecond * 500)
	}
	close(c)
}

// Experiencing channel deadlock.
func main() {
	c := make(chan string)
	c <- "hello world" // Causes deadlock
	msg := <-c
	fmt.Println(msg)
}
```
#### Output
```
~ go run .
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
```
Here, we confront the issue of channel deadlocks. While we might expect sending and receiving a message to work, we're greeted with a deadlock. Why? The send operation is blocking until there's a receiver. As there's no active receiver, the program is stuck in a deadlock.

### Lesson 7: Buffers for Unblocking
```go
// Utilizing buffered channels to prevent deadlocks.
func main() {
	c := make(chan string, 2)
	c <- "hello"
	c <- "world"

	msg := <-c
	fmt.Println(msg)

	msg = <-c
	fmt.Println(msg)
}
```
#### Output
```
~ go run .
hello
world
~
```
To circumvent deadlocks, we introduce buffered channels. A buffered channel allows sending values without blocking until the buffer is full. In lesson7, we create a buffered channel with a capacity of 2, enabling us to send two messages without encountering a deadlock.

### Lesson 8: Selecting from Channels
```go
// Employing select to choose from available channels.
func main() {
	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		for {
			time.Sleep(time.Millisecond * 500)
			c1 <- "Every 500ms"
		}
	}()

	go func() {
		for {
			time.Sleep(time.Second * 2)
			c2 <- "Every 2 seconds"
		}
	}()

	for {
		select {
		case msg1 := <-c1:
			fmt.Println(msg1)
		case msg2 := <-c2:
			fmt.Println(msg2)
		}
	}
}
```
#### Output
```
~ go run .
Every 500ms
Every 500ms
Every 500ms
Every 500ms
Every 2 seconds
Every 500ms
Every 500ms
```
The `select` statement comes to the rescue in this lesson. We have two channels, `c1` and `c2`, each receiving messages at different intervals. By using `select`, we can non-blockingly choose whichever channel is ready to send data, showcasing the versatility of this construct.

### Lesson 9: Building Worker Pools
```go
func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Println("worker", id, "started  job", j)
		time.Sleep(time.Second)
		fmt.Println("worker", id, "finished job", j)
		results <- j * 2
	}
}

// Creating worker pools for distributed tasks.
func main() {
	const numJobs = 5
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)

	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)

	for a := 1; a <= numJobs; a++ {
		<-results
	}
}
```
#### Output
```
~ go run .
worker 3 started  job 1
worker 2 started  job 3
worker 1 started  job 2
worker 1 finished job 2
worker 2 finished job 3
worker 3 finished job 1
worker 1 started  job 4
worker 2 started  job 5
worker 2 finished job 5
worker 1 finished job 4
~
```
Our final lesson delves into worker pools, a vital concept in concurrent programming. We define a `worker` function that processes jobs from a channel and sends results to another channel. In lesson9, we create a worker pool of three goroutines, distribute tasks, and collect results, effectively managing concurrent execution.

## Conclusion 🥂
As you wrap up this exploration, you're now armed with the prowess of concurrency through Go's goroutines and channels. Whether it's juggling diverse tasks, seamlessly exchanging information, or optimizing work sharing, you've delved into the heart of concurrent programming. This newfound ability empowers you to enhance your code's performance, responsiveness, and efficiency.