S1E1: Concurrency In Go | Goroutine | Channels | Waitgroup
Tags: go, concurrency, systemdesign, designpatterns
Published: 2023-07-23 11:45:32

I am happy to announce that I have started a new series on Concurrency Design patterns with the working code. This playlist will contain 9-10 coding + concept videos. For full detail explanation please go to the **CodePiper Youtube channel** video: https://youtu.be/eTJ7P0RCZJc

---

This blog will cover
1. Goroutines
2. Waitgroup
3. Channel
4. Buffered Channel
5. Deadlock in concurrency 

**Goroutine:**
Goroutines are the building blocks of concurrent programming in Go (often referred to as Golang). They are lightweight, independently executing functions that run concurrently with other goroutines in the same address space. Goroutines are managed by the Go runtime, and they utilize the available CPU cores efficiently through a technique called "multiplexing." You can think of a goroutine as a function that runs independently and concurrently with other functions, allowing for efficient and scalable concurrency in Go programs.

```go
func printNumbers() {
	for i := 1; i <= 5; i++ {
		fmt.Printf("%d ", i)
		time.Sleep(100 * time.Millisecond)
	}
}

func printLetters() {
	for ch := 'a'; ch <= 'e'; ch++ {
		fmt.Printf("%c ", ch)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go printNumbers() // Start a new Goroutine for printNumbers()
	printLetters()    // Execute printLetters() concurrently with the Goroutine
}

```

**WaitGroup:**
WaitGroup is a synchronization primitive provided by the Go standard library in the sync package. It is used to wait for a collection of goroutines to complete their execution before proceeding further in the main goroutine. A WaitGroup is essentially a counter that is incremented when a new goroutine is started and decremented when a goroutine finishes its task. The main goroutine can call Wait on the WaitGroup, which will block until the counter becomes zero (i.e., all goroutines have completed their tasks and called Done on the WaitGroup). It helps in coordinating the termination of multiple goroutines.
```go
func printNumbers(wg *sync.WaitGroup) {
	defer wg.Done() // Notify WaitGroup that this Goroutine is done when the function returns
	for i := 1; i <= 5; i++ {
		fmt.Printf("%d ", i)
		time.Sleep(100 * time.Millisecond)
	}
}

func printLetters(wg *sync.WaitGroup) {
	defer wg.Done() // Notify WaitGroup that this Goroutine is done when the function returns
	for ch := 'a'; ch <= 'e'; ch++ {
		fmt.Printf("%c ", ch)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2) // Add the number of Goroutines to wait for

	go printNumbers(&wg) // Start a new Goroutine for printNumbers()
	go printLetters(&wg) // Start a new Goroutine for printLetters()

	wg.Wait() // Wait until all Goroutines in the WaitGroup finish
	fmt.Print("Okay you can go")
}

```

**Channel:**
A channel in Go is a typed conduit that allows communication and synchronization between different goroutines. It is a way for goroutines to send and receive values to and from each other. Channels provide a safe way to exchange data and communicate between goroutines, preventing race conditions and other concurrency-related issues. Channels can be used to pass data from one goroutine to another, enabling the coordination of concurrent tasks effectively. Channels can be unbuffered or buffered, depending on their capacity.
```go
func sender(ch chan<- int) {
	for i := 1; i <= 10; i++ {
		ch <- i // Send data to the channel
	}
	close(ch) // Close the channel after sending all data
}

func receiver(ch <-chan int) {
	for num := range ch {
		fmt.Printf("%d ", num) // Receive data from the channel
	}
}

func main() {
	ch := make(chan int)

	go sender(ch) // Start a new Goroutine for the sender
	receiver(ch)  // Execute the receiver concurrently with the Goroutine
}
```

**Buffered Channel:**
A buffered channel is a type of channel in Go that has a fixed capacity to hold a certain number of elements. When you create a buffered channel, you specify the capacity as the second argument to the make function. For example, ch := make(chan int, 10). In this case, the channel ch can hold up to 10 elements. When a goroutine sends a value to a buffered channel, it will be added to the channel's internal buffer if there is space available. If the channel is full, the sending goroutine will block until there is room in the buffer. Similarly, when a goroutine tries to receive a value from a buffered channel, it will receive data from the buffer if it's not empty. If the buffer is empty, the receiving goroutine will block until data is available.
```go
func main() {
	ch := make(chan int, 3) // Create a buffered channel with a capacity of 3

	ch <- 1 // Send data to the channel
	ch <- 2 // Send more data
	ch <- 3 // Send even more data

	// ch <- 4 // Sending a 4th value would cause a deadlock because the channel is full

	fmt.Println(<-ch) // Receive data from the channel
	fmt.Println(<-ch) // Receive more data
	fmt.Println(<-ch) // Receive even more data

	// fmt.Println(<-ch) // Receiving a 4th value would cause a deadlock because the channel is empty
}
```

NOTE: _Using buffered channels can sometimes improve the performance of concurrent programs by reducing contention between goroutines, as long as the buffer size is chosen wisely based on the specific use case. However, it's essential to be mindful of buffer sizes to avoid consuming excessive memory._
```go
func producer(ch chan<- int) {
	for i := 1; i <= 5; i++ {
		fmt.Printf("Producing %d\n", i)
		ch <- i
		time.Sleep(500 * time.Millisecond) // Simulate some work by the producer
	}
	close(ch)
}

func consumer(ch <-chan int) {
	for num := range ch {
		fmt.Printf("Consuming %d\n", num)
		time.Sleep(1 * time.Second) // Simulate some work by the consumer
	}
}

func main() {
	normalCh := make(chan int)      // Normal channel
	bufferedCh := make(chan int, 3) // Buffered channel with capacity 3

	fmt.Println("Using Normal Channel:")
	go producer(normalCh)
	consumer(normalCh)

	time.Sleep(2 * time.Second) // Wait for the producer to finish

	fmt.Println("\nUsing Buffered Channel:")
	go producer(bufferedCh)
	consumer(bufferedCh)
}
```
```shell
Using Normal Channel:
Producing 1
Consuming 1
Producing 2
Consuming 2
Producing 3
Consuming 3
Producing 4
Consuming 4
Producing 5
Consuming 5

Using Buffered Channel:
Producing 1
Consuming 1
Producing 2
Consuming 2
Producing 3
Producing 4
Consuming 3
Producing 5
Consuming 4
Consuming 5
```

Youtube Channel: https://www.youtube.com/@codepiper
Github Link: https://github.com/arshad404/CodePiper/tree/main/GO/concurrency-patterns/Basics